From 16e490e30ebba815a813fa049ceda0c34607df85 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 00:35:02 +0700 Subject: [PATCH 01/20] feractor(clean): remove 0.5.0 source --- demo/.gitignore | 4 - demo/Cargo.lock | 889 --- demo/Cargo.toml | 9 - demo/config.yaml | 43 - demo/content/about.md | 6 - demo/content/blog/post1.md | 7 - demo/content/blog/post2.md | 7 - demo/content/index.md | 6 - demo/src/main.rs | 50 - demo/static/style.css | 17 - demo/templates/base.html | 34 - demo/templates/blog_list.html | 11 - demo/templates/blog_post.html | 7 - demo/templates/rss.xml | 16 - demo/templates/sitemap.xml | 11 - docs/.gitignore | 6 - docs/Cargo.lock | 780 --- docs/Cargo.toml | 7 - docs/config.yml | 43 - docs/content/code_of_conduct.md | 131 - docs/content/configuration.md | 423 -- docs/content/contributing.md | 655 -- docs/content/index.md | 48 - docs/content/installation.md | 243 - docs/content/license.md | 28 - docs/content/references.md | 538 -- docs/src/main.rs | 49 - docs/static/normalize.css | 351 -- docs/static/sidebar.css | 88 - docs/static/style.css | 6838 --------------------- docs/templates/base.html | 87 - docs/templates/rss.xml | 19 - docs/templates/sitemap.xml | 13 - tests/common/mod.rs | 227 - tests/common_mocks.rs | 43 - tests/config_tests.rs | 89 - tests/edge_cases_tests.rs | 54 - tests/error_tests.rs | 16 - tests/frontmatter_tests.rs | 79 - tests/fs_mock_tests.rs | 76 - tests/fs_real_tests.rs | 43 - tests/integration_advanced_tests.rs | 99 - tests/integration_test.rs | 162 - tests/markdown_tests.rs | 35 - tests/property_tests.proptest-regressions | 7 - tests/property_tests.rs | 32 - tests/serve_tests.rs | 74 - tests/site_builder_tests.rs | 148 - tests/site_page_tests.rs | 107 - tests/util_property_tests.rs | 19 - tests/util_tests.rs | 81 - tests/watcher_tests.rs | 20 - 52 files changed, 12875 deletions(-) delete mode 100644 demo/.gitignore delete mode 100644 demo/Cargo.lock delete mode 100644 demo/Cargo.toml delete mode 100644 demo/config.yaml delete mode 100644 demo/content/about.md delete mode 100644 demo/content/blog/post1.md delete mode 100644 demo/content/blog/post2.md delete mode 100644 demo/content/index.md delete mode 100644 demo/src/main.rs delete mode 100644 demo/static/style.css delete mode 100644 demo/templates/base.html delete mode 100644 demo/templates/blog_list.html delete mode 100644 demo/templates/blog_post.html delete mode 100644 demo/templates/rss.xml delete mode 100644 demo/templates/sitemap.xml delete mode 100644 docs/.gitignore delete mode 100644 docs/Cargo.lock delete mode 100644 docs/Cargo.toml delete mode 100644 docs/config.yml delete mode 100644 docs/content/code_of_conduct.md delete mode 100644 docs/content/configuration.md delete mode 100644 docs/content/contributing.md delete mode 100644 docs/content/index.md delete mode 100644 docs/content/installation.md delete mode 100644 docs/content/license.md delete mode 100644 docs/content/references.md delete mode 100644 docs/src/main.rs delete mode 100644 docs/static/normalize.css delete mode 100644 docs/static/sidebar.css delete mode 100644 docs/static/style.css delete mode 100644 docs/templates/base.html delete mode 100644 docs/templates/rss.xml delete mode 100644 docs/templates/sitemap.xml delete mode 100644 tests/common/mod.rs delete mode 100644 tests/common_mocks.rs delete mode 100644 tests/config_tests.rs delete mode 100644 tests/edge_cases_tests.rs delete mode 100644 tests/error_tests.rs delete mode 100644 tests/frontmatter_tests.rs delete mode 100644 tests/fs_mock_tests.rs delete mode 100644 tests/fs_real_tests.rs delete mode 100644 tests/integration_advanced_tests.rs delete mode 100644 tests/integration_test.rs delete mode 100644 tests/markdown_tests.rs delete mode 100644 tests/property_tests.proptest-regressions delete mode 100644 tests/property_tests.rs delete mode 100644 tests/serve_tests.rs delete mode 100644 tests/site_builder_tests.rs delete mode 100644 tests/site_page_tests.rs delete mode 100644 tests/util_property_tests.rs delete mode 100644 tests/util_tests.rs delete mode 100644 tests/watcher_tests.rs diff --git a/demo/.gitignore b/demo/.gitignore deleted file mode 100644 index 1d96de9..0000000 --- a/demo/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target -/dev -.velodiff_dist.diff -.snapcat.md \ No newline at end of file diff --git a/demo/Cargo.lock b/demo/Cargo.lock deleted file mode 100644 index 7ac70d1..0000000 --- a/demo/Cargo.lock +++ /dev/null @@ -1,889 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "futures-core" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" - -[[package]] -name = "futures-task" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" - -[[package]] -name = "futures-util" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width 0.2.2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "is_ci" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "librawssg" -version = "0.5.0" -dependencies = [ - "chrono", - "glob", - "miette", - "pulldown-cmark", - "serde", - "serde_yaml", - "tera", - "thiserror", - "tracing", - "walkdir", -] - -[[package]] -name = "librawssg-demo" -version = "0.1.0" -dependencies = [ - "librawssg", - "tempfile", - "tracing-subscriber", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "backtrace", - "backtrace-ext", - "cfg-if", - "miette-derive", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "owo-colors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pulldown-cmark" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "getopts", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rustc-demangle" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "supports-color" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" -dependencies = [ - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" - -[[package]] -name = "supports-unicode" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "tera" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511f07fd91a70e92efbe4793d111aaa9035f8474dd157aaa1e31e7c27f5051da" -dependencies = [ - "serde", -] - -[[package]] -name = "terminal_size" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" -dependencies = [ - "rustix", - "windows-sys", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "unicode-linebreak", - "unicode-width 0.2.2", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] diff --git a/demo/Cargo.toml b/demo/Cargo.toml deleted file mode 100644 index 5b05d0f..0000000 --- a/demo/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "librawssg-demo" -version = "0.1.0" -edition = "2024" - -[dependencies] -librawssg = { path = "..", features = ["tera", "pulldown"] } -tempfile = "3" -tracing-subscriber = "0.3.23" diff --git a/demo/config.yaml b/demo/config.yaml deleted file mode 100644 index 5a6ef1d..0000000 --- a/demo/config.yaml +++ /dev/null @@ -1,43 +0,0 @@ -site: - site_name: "My librawssg Site" - description: "A static site built with librawssg" - language: "id" - base_url: "https://example.com" - author: "Your Name" - navbar: - - label: "Beranda" - url: "/" - - label: "Tentang" - url: "/about.html" - - label: "Blog" - url: "/blog/index.html" - sidebar: - - label: "RSS" - url: "/rss.xml" - -build: - content_dir: "content" - output_dir: "./dist" - templates_dir: "templates" - static_dir: "static" - -content_types: - - name: "page" - pattern: "*.md" - template: "base.html" - list_enabled: false - - name: "blog" - pattern: "blog/*.md" - template: "blog_post.html" - list_template: "blog_list.html" - list_enabled: true - -generators: - rss: - enabled: false - path: "rss.xml" - template: "rss.xml" - sitemap: - enabled: false - path: "sitemap.xml" - template: "sitemap.xml" diff --git a/demo/content/about.md b/demo/content/about.md deleted file mode 100644 index 1502dd0..0000000 --- a/demo/content/about.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: About -desc: About this site ---- - -This site is built with librawssg, a super stable static site generator kernel. diff --git a/demo/content/blog/post1.md b/demo/content/blog/post1.md deleted file mode 100644 index e2aad31..0000000 --- a/demo/content/blog/post1.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: First Blog Post -desc: The very first post -date: 2025-01-01 ---- - -This is my first blog post. librawssg handles Markdown perfectly. diff --git a/demo/content/blog/post2.md b/demo/content/blog/post2.md deleted file mode 100644 index 8bdc823..0000000 --- a/demo/content/blog/post2.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Second Blog Post -desc: Another interesting post -date: 2025-02-15 ---- - -Second post with **bold text** and [a link](https://github.com/mroczect/librawssg). diff --git a/demo/content/index.md b/demo/content/index.md deleted file mode 100644 index e71f16e..0000000 --- a/demo/content/index.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Home -desc: Welcome to my static site ---- - -This is the home page, generated by **librawssg**. diff --git a/demo/src/main.rs b/demo/src/main.rs deleted file mode 100644 index a8fbb73..0000000 --- a/demo/src/main.rs +++ /dev/null @@ -1,50 +0,0 @@ -use librawssg::markdown::PulldownMarkdown; -use librawssg::site::context::{TeraFeedContextBuilder, TeraSitemapContextBuilder}; -use librawssg::site::TeraRenderer; -use librawssg::SiteBuilder; -use std::path::Path; - -fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - - let builder = SiteBuilder::new() - .load_config("config.yaml") - .expect("Gagal memuat config.yaml"); - - let mut tera = TeraRenderer::new(); - let templates_dir = Path::new("templates"); - if templates_dir.exists() { - for entry in std::fs::read_dir(templates_dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_file() { - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - let content = std::fs::read_to_string(&path)?; - tera.add_raw_template(&name, &content)?; - } - } - } - - let md = PulldownMarkdown; - let content_dir = Path::new("content"); - let output_dir = Path::new("./dist"); - - let site = builder - .content_dir(content_dir) - .output_dir(output_dir) - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .with_feed_context_builder(Box::new(TeraFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(TeraSitemapContextBuilder)) - .build()?; - - println!("Jumlah halaman: {}", site.pages().len()); - for page in site.pages() { - println!(" {} -> {}", page.file_path, page.url); - } - - site.generate()?; - println!("Situs berhasil dibuat di {}", output_dir.display()); - - Ok(()) -} diff --git a/demo/static/style.css b/demo/static/style.css deleted file mode 100644 index c85f439..0000000 --- a/demo/static/style.css +++ /dev/null @@ -1,17 +0,0 @@ -body { - font-family: sans-serif; - max-width: 800px; - margin: 0 auto; - padding: 1rem; -} -header { - border-bottom: 1px solid #ccc; - margin-bottom: 1rem; -} -footer { - margin-top: 2rem; - padding-top: 1rem; - border-top: 1px solid #ccc; - font-size: 0.8rem; - color: #666; -} diff --git a/demo/templates/base.html b/demo/templates/base.html deleted file mode 100644 index 67feb70..0000000 --- a/demo/templates/base.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - {{ page_title }} - {{ site.site_name }} - - - - -
-

{{ site.site_name }}

- -
- {% if site.sidebar %} - - {% endif %} -
- {% block content %} -

{{ page_title }}

- {{ page_content | safe }} {% endblock %} -
-
- Dibuat dengan librawssg -
- - diff --git a/demo/templates/blog_list.html b/demo/templates/blog_list.html deleted file mode 100644 index 6bdd8d6..0000000 --- a/demo/templates/blog_list.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} {% block content %} -

{{ page_title }}

- -{% endblock %} diff --git a/demo/templates/blog_post.html b/demo/templates/blog_post.html deleted file mode 100644 index 6837922..0000000 --- a/demo/templates/blog_post.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} {% block content %} -
-

{{ page_title }}

-

{{ page_pub_date }}

- {{ page_content | safe }} -
-{% endblock %} diff --git a/demo/templates/rss.xml b/demo/templates/rss.xml deleted file mode 100644 index 4915208..0000000 --- a/demo/templates/rss.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - {{ site.site_name }} - {{ site.description }} - {{ base_url }} - {% for post in posts %} - - {{ post.frontmatter.title }} - {{ base_url }}/{{ post.url }} - {{ post.frontmatter.desc }} - {{ post.pub_date }} - - {% endfor %} - - diff --git a/demo/templates/sitemap.xml b/demo/templates/sitemap.xml deleted file mode 100644 index 282c0f4..0000000 --- a/demo/templates/sitemap.xml +++ /dev/null @@ -1,11 +0,0 @@ - - -{% for page in pages %} - - {{ base_url }}/{{ page.url }} - {% if page.pub_date %} - {{ page.pub_date }} - {% endif %} - -{% endfor %} - \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 5b5ef38..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/target -/dev -.velodiff_dist.diff -.snapcat.md -/dist -/dist.tmp \ No newline at end of file diff --git a/docs/Cargo.lock b/docs/Cargo.lock deleted file mode 100644 index dcf8a13..0000000 --- a/docs/Cargo.lock +++ /dev/null @@ -1,780 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "futures-core" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" - -[[package]] -name = "futures-task" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" - -[[package]] -name = "futures-util" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width 0.2.2", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "is_ci" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "librawssg" -version = "0.5.0" -dependencies = [ - "chrono", - "glob", - "miette", - "pulldown-cmark", - "serde", - "serde_yaml", - "tera", - "thiserror", - "tracing", - "walkdir", -] - -[[package]] -name = "librawssg-docs" -version = "0.1.0" -dependencies = [ - "librawssg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "backtrace", - "backtrace-ext", - "cfg-if", - "miette-derive", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "owo-colors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pulldown-cmark" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "getopts", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "supports-color" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" -dependencies = [ - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" - -[[package]] -name = "supports-unicode" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tera" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511f07fd91a70e92efbe4793d111aaa9035f8474dd157aaa1e31e7c27f5051da" -dependencies = [ - "serde", -] - -[[package]] -name = "terminal_size" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" -dependencies = [ - "rustix", - "windows-sys", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "unicode-linebreak", - "unicode-width 0.2.2", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] diff --git a/docs/Cargo.toml b/docs/Cargo.toml deleted file mode 100644 index ef4f6b3..0000000 --- a/docs/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "librawssg-docs" -version = "0.1.0" -edition = "2024" - -[dependencies] -librawssg = { path = "..", features = ["tera", "pulldown"] } diff --git a/docs/config.yml b/docs/config.yml deleted file mode 100644 index e6bae1e..0000000 --- a/docs/config.yml +++ /dev/null @@ -1,43 +0,0 @@ -site: - site_name: "librawssg" - description: "Safety-first kernel for static site generation in Rust" - language: "en" - base_url: "https://mroczect.biz.id/librawssg" - author: "mroczect" - repo_url: "https://github.com/mroczect/librawssg" - license: "MIT" - - # Sidebar digunakan untuk daftar isi di dalam halaman - sidebar: - - label: "Home" - url: "index.html" - - label: "Installation" - url: "installation.html" - - label: "Configuration" - url: "configuration.html" - - label: "API Reference" - url: "references.html" - - label: "Contributing" - url: "contributing.html" - - label: "Code of Conduct" - url: "code_of_conduct.html" - - label: "License" - url: "license.html" - -build: - content_dir: "content" - output_dir: "dist" - templates_dir: "templates" - static_dir: "static" - -content_types: - - name: "page" - pattern: "*.md" - template: "base.html" - list_enabled: false - -generators: - rss: - enabled: false - sitemap: - enabled: false diff --git a/docs/content/code_of_conduct.md b/docs/content/code_of_conduct.md deleted file mode 100644 index 711f8f0..0000000 --- a/docs/content/code_of_conduct.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Contributor Covenant Code of Conduct -desc: Community guidelines for librawssg ---- - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -- Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -mroczect@proton.me. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. diff --git a/docs/content/configuration.md b/docs/content/configuration.md deleted file mode 100644 index 8145ab3..0000000 --- a/docs/content/configuration.md +++ /dev/null @@ -1,423 +0,0 @@ ---- -title: Configuration -desc: Complete YAML configuration reference for librawssg ---- - -librawssg is configured through a single YAML file. Every aspect of the site – metadata, directory layout, content types, and generators – is defined here. - -The library ships with sensible defaults, so you only need to specify what changes from the default behaviour. - ---- - -## Loading Configuration - -There are two ways to provide configuration: - -### From a YAML file (recommended) - -```rust -use librawssg::site::builder::SiteBuilder; - -let site = SiteBuilder::new() - .load_config("config.yml")? - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .build()?; -``` - -`load_config` reads, parses, and **validates** the file before returning. Any error (missing file, invalid YAML, failed validation) is returned immediately. - -### Programmatically - -```rust -use librawssg::types::RawssgConfig; - -let mut config = RawssgConfig::default(); -config.site.site_name = "My Site".into(); -config.build.content_dir = "pages".into(); -config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "**/*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, -}); - -let site = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .build()?; -``` - -Use this approach when you need to generate configuration dynamically or don't want a separate YAML file. - ---- - -## Validation - -When you call `load_config` or `build`, the configuration is automatically validated. The following rules are enforced: - -| Rule | Error message | -| ------------------------------------------------------------------------------ | ----------------------------------------------------- | -| `site.site_name` must not be empty | `"site.name cannot be empty"` | -| At least one `content_type` must be defined | `"at least one content_type must be defined"` | -| Every content type must have a non‑empty `name` | `"content_type name cannot be empty"` | -| Every content type must have a non‑empty `template` | `"content_type '...' must have a template"` | -| Every content type `pattern` must be a valid glob | `"Invalid glob pattern '...'"` | -| If RSS is enabled, `rss.template` and `rss.path` must be non‑empty | `"rss.template is required when RSS enabled"` | -| If sitemap is enabled, `sitemap.template` and `sitemap.path` must be non‑empty | `"sitemap.template is required when sitemap enabled"` | - -Validation happens **before** any file processing, saving you from errors later in the build. - ---- - -## Full YAML Structure - -```yaml -site: # Site-wide metadata (available in templates as `site.xxx`) - site_name: # Required. Must not be empty. - description: # Optional. - language: # Optional. Default: "en" - base_url: # Optional. Used in RSS/sitemap. - author: # Optional. - repo_url: # Optional. - license: # Optional. - navbar: # Optional. List of NavItem. - sidebar: # Optional. List of NavItem. - -build: # Directory layout - content_dir: # Default: "content" - output_dir: # Default: "dist" - templates_dir: # Default: "templates" - static_dir: # Default: "static" - -content_types: # List of content type definitions (at least one required) - - name: # Required. Identifier (e.g. "blog", "page") - pattern: # Required. Glob pattern (e.g. "blog/*.md") - template: # Required. Template name (e.g. "post.html") - list_template: # Optional. Template for list pages - list_enabled: # Optional. Default: false - -generators: # Automatic RSS and sitemap generation - rss: - enabled: # Default: true - path: # Required if enabled. Output path (e.g. "rss.xml") - template: # Required if enabled. Template name (e.g. "rss.xml") - sitemap: - enabled: # Default: true - path: # Required if enabled. Output path (e.g. "sitemap.xml") - template: # Required if enabled. Template name (e.g. "sitemap.xml") -``` - ---- - -## `site` – Site Metadata - -All fields under `site` are accessible in templates under the `site` object. For example, `site.site_name`, `site.author`, `site.navbar`. - -### Fields - -| Field | Type | Required | Default | Description | -| ------------- | -------- | -------- | ---------- | ---------------------------------------------- | -| `site_name` | `string` | **Yes** | `"rawssg"` | Name of the site. Used in ``, RSS, etc. | -| `description` | `string` | No | `None` | Short description. Used in meta tags and RSS. | -| `language` | `string` | No | `"en"` | HTML language code (e.g. `"id"`, `"fr"`). | -| `base_url` | `string` | No | `None` | Full public URL. Used in RSS and sitemap. | -| `author` | `string` | No | `None` | Author name. Available as `site.author`. | -| `repo_url` | `string` | No | `None` | Repository URL. Available as `site.repo_url`. | -| `license` | `string` | No | `None` | License name. Available as `site.license`. | -| `navbar` | `list` | No | `[]` | Navigation bar items. See [NavItem](#navitem). | -| `sidebar` | `list` | No | `[]` | Sidebar items. See [NavItem](#navitem). | - -### Example - -```yaml -site: - site_name: "My Awesome Blog" - description: "Thoughts about Rust and static sites" - language: "en" - base_url: "https://example.com" - author: "Jane Doe" - repo_url: "https://github.com/janedoe/my-blog" - license: "MIT" - navbar: - - label: "Home" - url: "/" - - label: "About" - url: "/about.html" - - label: "Blog" - url: "/blog/index.html" - sidebar: - - label: "RSS" - url: "/rss.xml" - - label: "GitHub" - url: "https://github.com/janedoe/my-blog" -``` - -### NavItem - -| Field | Type | Description | -| ------- | -------- | ---------------------------------- | -| `label` | `string` | Display text for the link | -| `url` | `string` | Link target (relative or absolute) | - ---- - -## `build` – Directory Layout - -Controls where the library looks for input files and where it writes output. - -### Fields - -| Field | Type | Default | Description | -| --------------- | -------- | ------------- | ---------------------------------------------------------- | -| `content_dir` | `string` | `"content"` | Directory containing Markdown files | -| `output_dir` | `string` | `"dist"` | Directory where generated HTML is written | -| `templates_dir` | `string` | `"templates"` | Directory containing template files (if loading from disk) | -| `static_dir` | `string` | `"static"` | Directory with static assets (copied as-is) | - -### Example - -```yaml -build: - content_dir: "my-content" - output_dir: "public" - templates_dir: "my-templates" - static_dir: "my-static" -``` - -### Notes - -- All paths are relative to the **current working directory** when the binary runs. -- The `templates_dir` is **not used** by the library directly. You must load templates yourself (see the demo or docs site for an example). The field is kept for reference and future use. -- The `static_dir` is processed automatically: every file inside is copied to the output directory. -- Non‑Markdown files in `content_dir` (images, CSS, JS) are also copied automatically. - ---- - -## `content_types` – Content Type Definitions - -This is the heart of the content pipeline. Every Markdown file must be matched by at least one content type. - -### Fields - -| Field | Type | Required | Default | Description | -| --------------- | -------- | -------- | ------- | -------------------------------------------------------------------------------------------- | -| `name` | `string` | **Yes** | – | Identifier for this type (e.g. `"blog"`, `"page"`). Used as `content_type` in `PageContext`. | -| `pattern` | `string` | **Yes** | – | Glob pattern relative to `content_dir`. Supports `*` (one segment) and `**` (any segments). | -| `template` | `string` | **Yes** | – | Name of the template used for single pages of this type. | -| `list_template` | `string` | No | `None` | Template for list pages (e.g. `blog/index.html`). | -| `list_enabled` | `bool` | No | `false` | If `true`, a list page is generated automatically. | - -### How patterns work - -| Pattern | Matches | Does not match | -| -------------- | ----------------------------------- | ------------------------------- | -| `*.md` | `index.md`, `about.md` | `blog/post.md` | -| `blog/*.md` | `blog/post.md` | `blog/2024/post.md`, `index.md` | -| `blog/**/*.md` | `blog/post.md`, `blog/2024/post.md` | `index.md` | -| `**/*.md` | Everything ending in `.md` | – | - -Patterns are relative to `content_dir`. They use a custom glob engine that supports `*` and `**` wildcards. - -### How list pages work - -When `list_enabled` is `true` and `list_template` is set: - -1. All pages of that type are collected. -2. A `PageContext` with `is_list = true` is created. -3. The list of items is available in the template as the `pages` variable. - -Example template (`blog_list.html`): - -```html -{% extends "base.html" %} {% block content %} -<h2>All Posts</h2> -<ul> - {% for post in pages %} - <li> - <a href="{{ base_path }}{{ post.url }}">{{ post.frontmatter.title }}</a> - <span>({{ post.pub_date }})</span> - </li> - {% endfor %} -</ul> -{% endblock %} -``` - -The list page is generated at `{name}/index.html` (e.g. `blog/index.html`). - -### Example - -```yaml -content_types: - - name: "page" - pattern: "*.md" - template: "base.html" - # No list for regular pages - - - name: "blog" - pattern: "blog/*.md" - template: "blog_post.html" - list_template: "blog_list.html" - list_enabled: true - - - name: "tutorial" - pattern: "tutorials/**/*.md" - template: "tutorial.html" - list_template: "tutorial_list.html" - list_enabled: true -``` - ---- - -## `generators` – RSS and Sitemap - -Automatically generate RSS and sitemap files for your site. - -### RSS - -| Field | Type | Default | Description | -| ---------- | -------- | ------- | ------------------------------------------------------- | -| `enabled` | `bool` | `true` | Whether to generate an RSS feed | -| `path` | `string` | – | Output path relative to `output_dir` (e.g. `"rss.xml"`) | -| `template` | `string` | – | Template name for the RSS XML | - -The RSS feed collects all pages with `content_type == "blog"` (sorted by date, newest first) and passes them as the `posts` variable to the template. - -Example RSS template: - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<rss version="2.0"> - <channel> - <title>{{ site.site_name }} - {{ site.description }} - {{ base_url }} - {% for post in posts %} - - {{ post.frontmatter.title }} - {{ base_url }}/{{ post.url }} - {{ post.frontmatter.desc }} - {{ post.pub_date }} - - {% endfor %} - - -``` - -### Sitemap - -| Field | Type | Default | Description | -| ---------- | -------- | ------- | ----------------------------------------------------------- | -| `enabled` | `bool` | `true` | Whether to generate a sitemap | -| `path` | `string` | – | Output path relative to `output_dir` (e.g. `"sitemap.xml"`) | -| `template` | `string` | – | Template name for the sitemap XML | - -All pages (including list pages) are passed as the `pages` variable. - -Example sitemap template: - -```xml - - -{% for page in pages %} - - {{ base_url }}/{{ page.url }} - {% if page.pub_date %} - {{ page.pub_date }} - {% endif %} - -{% endfor %} - -``` - -### Example configuration - -```yaml -generators: - rss: - enabled: true - path: "feed.xml" - template: "rss.xml" - sitemap: - enabled: true - path: "sitemap.xml" - template: "sitemap.xml" -``` - -To disable a generator entirely: - -```yaml -generators: - rss: - enabled: false - sitemap: - enabled: false -``` - ---- - -## Complete Example - -Here is a full, annotated configuration file that you can use as a starting point: - -```yaml -# ============================================================================= -# librawssg Configuration -# ============================================================================= - -# Site metadata – available in templates as `site.xxx` -site: - site_name: "My Site" - description: "A blog about Rust" - language: "en" - base_url: "https://example.com" - author: "Your Name" - repo_url: "https://github.com/you/your-site" - license: "MIT" - - navbar: - - label: "Home" - url: "/" - - label: "About" - url: "/about.html" - - label: "Blog" - url: "/blog/index.html" - - sidebar: - - label: "RSS" - url: "/rss.xml" - -# Directory paths (all relative to working directory) -build: - content_dir: "content" - output_dir: "dist" - templates_dir: "templates" - static_dir: "static" - -# Content type definitions -content_types: - - name: "page" - pattern: "*.md" - template: "base.html" - - - name: "blog" - pattern: "blog/*.md" - template: "blog_post.html" - list_template: "blog_list.html" - list_enabled: true - -# RSS and Sitemap -generators: - rss: - enabled: true - path: "rss.xml" - template: "rss.xml" - sitemap: - enabled: true - path: "sitemap.xml" - template: "sitemap.xml" -``` - -Save this as `config.yml` in your project root, then load it with `SiteBuilder::new().load_config("config.yml")`. diff --git a/docs/content/contributing.md b/docs/content/contributing.md deleted file mode 100644 index 0924741..0000000 --- a/docs/content/contributing.md +++ /dev/null @@ -1,655 +0,0 @@ ---- -title: Contributing -desc: How to contribute to librawssg ---- - -Thank you for your interest in contributing to **librawssg**. -We welcome contributions of all kinds: bug reports, feature requests, documentation improvements, code patches, and design discussions. - -This guide outlines the entire contribution process so that everyone can work together effectively. - ---- - -## Table of Contents - -- [Code of Conduct](#code-of-conduct) -- [Ways to Contribute](#ways-to-contribute) -- [Getting Started](#getting-started) -- [Development Environment](#development-environment) -- [Project Structure](#project-structure) -- [Building and Testing](#building-and-testing) -- [Coding Style](#coding-style) -- [Commit Messages](#commit-messages) -- [Pull Request Process](#pull-request-process) -- [Reporting Bugs](#reporting-bugs) -- [Feature Requests](#feature-requests) -- [Documentation](#documentation) -- [Community](#community) -- [Recognition](#recognition) - ---- - -## Code of Conduct - -This project adheres to a minimal set of social rules: - -- **Be respectful** – treat others as you would like to be treated. -- **Be constructive** – focus on the issue, not the person. -- **Be inclusive** – welcome people of all backgrounds and experience levels. - -Harassment, discrimination, or hostile behaviour is not tolerated. -If you experience or witness such conduct, please contact the maintainers immediately. - -Read the full [Code of Conduct](code_of_conduct.html). - ---- - -## Ways to Contribute - -You don't have to write code to make a difference. Here are some ways you can help: - -| Contribution type | Impact | -| -------------------- | --------------------------------------------------- | -| **Bug reports** | Help us find and fix problems | -| **Feature requests** | Shape the future of the library | -| **Documentation** | Make the library easier to learn | -| **Code patches** | Fix bugs, add features, improve performance | -| **Code review** | Help maintain quality by reviewing PRs | -| **Testing** | Write tests, report edge cases | -| **Spread the word** | Write blog posts, give talks, share on social media | - ---- - -## Getting Started - -### 1. Fork the repository - -Click the **Fork** button at the top of [github.com/mroczect/librawssg](https://github.com/mroczect/librawssg). - -### 2. Clone your fork - -```bash -git clone https://github.com/YOUR_USERNAME/librawssg.git -cd librawssg -``` - -### 3. Add the upstream remote - -This keeps your fork in sync with the main repository. - -```bash -git remote add upstream https://github.com/mroczect/librawssg.git -``` - -### 4. Create a branch - -Use a descriptive name that reflects what you're working on. - -```bash -git checkout -b feat/my-awesome-feature -``` - -Branch naming conventions: - -| Prefix | Purpose | Example | -| ----------- | ------------------ | ---------------------- | -| `feat/` | New feature | `feat/custom-handlers` | -| `fix/` | Bug fix | `fix/path-traversal` | -| `docs/` | Documentation | `docs/api-reference` | -| `test/` | Adding tests | `test/site-builder` | -| `refactor/` | Code restructuring | `refactor/error-types` | -| `chore/` | Maintenance tasks | `chore/update-deps` | - -### 5. Keep your branch up to date - -```bash -git fetch upstream -git rebase upstream/master -``` - -Rebase instead of merge to keep the history clean. -If you're uncomfortable with rebasing, merging is also acceptable. - ---- - -## Development Environment - -### Required tools - -- **Rust** – Install via [rustup](https://rustup.rs). The latest stable version is recommended. - ```bash - rustup update stable - ``` -- **Cargo** – Comes with Rust. No additional setup needed. -- **Git** – For version control. - -### Recommended tools - -- **cargo-edit** – Easily manage dependencies from the command line. - ```bash - cargo install cargo-edit - ``` -- **cargo-watch** – Automatically rebuild and test on file changes. - ```bash - cargo install cargo-watch - ``` -- **cargo-audit** – Check for security vulnerabilities in dependencies. - ```bash - cargo install cargo-audit - ``` - -### Editor setup - -Any text editor works, but we recommend one with Rust support: - -- **VS Code** with the `rust-analyzer` extension -- **IntelliJ IDEA** with the Rust plugin -- **Helix** or **Neovim** with built-in LSP support - -Configure your editor to: - -- Run `rustfmt` on save -- Show Clippy warnings inline -- Enable `rust-analyzer` for autocompletion and type hints - ---- - -## Project Structure - -Understanding the codebase helps you make better contributions. - -``` -librawssg/ -├── src/ # Library source code -│ ├── config/ # Configuration loading (YAML, defaults) -│ │ ├── mod.rs # ConfigLoader trait -│ │ └── loader.rs # YamlConfigLoader implementation -│ ├── error.rs # RawssgError enum (thiserror + miette) -│ ├── frontmatter.rs # YAML frontmatter extraction -│ ├── fs/ # Filesystem abstraction -│ │ ├── mod.rs # FileSystem trait -│ │ └── real.rs # RealFs implementation -│ ├── markdown.rs # MarkdownRenderer trait (+ optional pulldown) -│ ├── serve/ # Optional dev server -│ │ ├── mod.rs # Static file server -│ │ └── watcher.rs # File watcher (notify) -│ ├── site/ # Core site building logic -│ │ ├── mod.rs # TemplateRenderer trait, ContentHandler trait -│ │ ├── builder.rs # SiteBuilder and Site -│ │ ├── page.rs # Page context building -│ │ ├── feed.rs # RSS feed generation -│ │ └── sitemap.rs # Sitemap generation -│ ├── types.rs # All data types (config, frontmatter, context) -│ ├── util.rs # Path safety, slugify, glob matching -│ └── lib.rs # Crate root, re-exports -├── tests/ # Integration tests -│ ├── common/ # Mock implementations (MockFs, MockRenderers) -│ │ └── mod.rs -│ ├── config_tests.rs -│ ├── error_tests.rs -│ ├── frontmatter_tests.rs -│ ├── fs_mock_tests.rs -│ ├── fs_real_tests.rs -│ ├── integration_test.rs -│ ├── markdown_tests.rs -│ ├── site_builder_tests.rs -│ ├── site_page_tests.rs -│ └── util_tests.rs -├── demo/ # Full working example -├── docs/ # Documentation site source -└── Cargo.toml # Project manifest -``` - -### Key design principles - -1. **Everything is a trait** – Filesystem, rendering, content processing are all behind traits. This makes the library testable and extensible. -2. **Minimal dependencies** – Only essential crates are required. Everything else is optional. -3. **Safety first** – Path traversal is prevented at every level. Output is atomic. -4. **Validate early** – Configuration errors are caught at build time, not generation time. - ---- - -## Building and Testing - -All commands are run from the repository root. - -### Build - -```bash -# Basic build (no optional features) -cargo build - -# Build with all features -cargo build --all-features - -# Release build (optimised) -cargo build --release -``` - -### Run tests - -```bash -# Run all tests (only tests that don't require features) -cargo test - -# Run tests with Tera and pulldown features -cargo test --features tera,pulldown - -# Run tests with dev server feature -cargo test --features serve - -# Run all tests with every feature -cargo test --all-features - -# Run a specific test file -cargo test --test config_tests - -# Run a specific test function -cargo test test_name -``` - -### Format and lint - -```bash -# Check formatting (CI will fail if this isn't clean) -cargo fmt --all -- --check - -# Auto-format all code -cargo fmt --all - -# Run Clippy with strict warnings -cargo clippy --all-targets --all-features -- -D warnings -``` - -**These checks are enforced in CI.** Run them locally before pushing. - -### Watch mode (auto-test on changes) - -```bash -cargo watch -x "test --all-features" -x "clippy --all-targets --all-features -- -D warnings" -``` - ---- - -## Coding Style - -We follow the standard Rust style guide with a few additional conventions. - -### General - -- Follow what `cargo fmt` produces. No arguments. -- All Clippy warnings are errors. If Clippy complains, fix it. -- Keep functions small – aim for under 50 lines. -- One responsibility per function. -- Use meaningful variable names. - -### Error handling - -- Use `Result` for all fallible operations. -- Use the `?` operator liberally. -- Add context to errors where helpful: - ```rust - fs.read_to_string(path) - .map_err(|e| RawssgError::Config(format!("cannot read config: {}", e)))?; - ``` -- Don't use `.unwrap()` or `.expect()` in library code unless you can prove the invariant. - -### Documentation - -- Every public item must have a `///` doc comment. -- Include a short description, not just a repetition of the name. -- Add a code example when the usage isn't obvious. - -Example: - -````rust -/// Loads a site configuration from a YAML file. -/// -/// The file is validated after loading. Returns an error if the file -/// cannot be read or contains invalid YAML. -/// -/// # Example -/// ```rust -/// let builder = SiteBuilder::new() -/// .load_config("config.yml") -/// .expect("failed to load config"); -/// ``` -pub fn load_config>(mut self, path: P) -> Result { - // ... -} -```` - -### Testing - -- Write tests for all new functionality. -- Place unit tests in the same file as the code they test, inside a `#[cfg(test)]` module. -- Place integration tests in the `tests/` directory. -- Use the mock implementations in `tests/common/mod.rs` for testing without real files. - -### Performance - -- Don't optimise prematurely. -- If you're making a performance claim, include benchmarks. -- Use `cargo bench` if benchmarks are available. - ---- - -## Commit Messages - -We use the [Conventional Commits](https://www.conventionalcommits.org/) format. - -### Format - -``` -type(scope): short description - -Optional longer explanation of the change, including motivation -and any breaking changes. -``` - -### Types - -| Type | When to use | -| ---------- | ----------------------------------------------------- | -| `feat` | A new feature | -| `fix` | A bug fix | -| `docs` | Documentation changes | -| `test` | Adding or updating tests | -| `refactor` | Code changes that neither fix a bug nor add a feature | -| `chore` | Maintenance tasks (dependencies, CI, etc.) | -| `style` | Formatting, missing semicolons, etc. (no code change) | -| `perf` | Performance improvements | - -### Scope - -The scope is usually `librawssg`, but can be more specific: - -| Scope | When to use | -| ----------- | --------------------------- | -| `librawssg` | Changes to the core library | -| `ci` | CI pipeline changes | -| `docs` | Documentation site changes | -| `demo` | Demo project changes | - -### Examples - -```text -feat(librawssg): add support for custom content handlers - -Introduce the ContentHandler trait that allows users to define their own -file processing logic. Includes default MarkdownPageHandler. - -BREAKING CHANGE: SiteBuilder::new() no longer sets up default handlers. -``` - -```text -fix(librawssg): prevent path traversal in safe_path - -safe_path now normalises paths before checking boundaries, preventing -attacks using ../ sequences on not-yet-existing files. -``` - -```text -docs(librawssg): add API reference for PageContext - -Document all fields of PageContext with examples showing how they are -populated from Markdown frontmatter. -``` - ---- - -## Pull Request Process - -### Before you submit - -1. **Update your branch** – Rebase onto the latest `master`. - - ```bash - git fetch upstream - git rebase upstream/master - ``` - -2. **Run all checks** – Make sure everything passes. - - ```bash - cargo fmt --all -- --check - cargo clippy --all-targets --all-features -- -D warnings - cargo test --all-features - ``` - -3. **Check your commits** – Squash trivial commits, ensure messages follow the convention. - -### Submitting - -1. Push your branch to your fork. -2. Open a pull request against `mroczect/librawssg:master`. -3. Fill out the PR template completely. - -### PR description - -Your PR description should answer these questions: - -- **What** does this change do? -- **Why** is this change needed? -- **How** was it implemented (brief overview)? -- **Breaking changes** – List any changes that break existing code. -- **Testing** – Describe how you tested the change. -- **Related issues** – Link any issues this PR addresses. - -Example: - -```markdown -## What - -Add a `ContentHandler` trait that allows users to define custom file -processing logic. - -## Why - -Currently, only Markdown files can be processed. Users with other file -types (AsciiDoc, reStructuredText) have no way to integrate them. - -## How - -- Defined the `ContentHandler` trait in `site/mod.rs` -- Added `add_handler()` method to `SiteBuilder` -- Converted existing Markdown handling into `MarkdownPageHandler` -- Added `StaticFileHandler` as a catch-all - -## Breaking changes - -- `SiteBuilder::new()` no longer sets up default handlers. - Users must call `.add_handler()` if they remove defaults. - -## Testing - -- Added `test_custom_handler()` to `tests/site_builder_tests.rs` -- All existing tests pass with the updated builder - -## Related issues - -Closes #42 -``` - -### Review process - -1. A maintainer will review your code within a few days. -2. They may ask questions or request changes. -3. Make the requested changes in new commits, then push. -4. Once approved, the maintainer will merge your PR via **squash merge** to keep the history linear. - -### After merge - -- Delete your branch. -- Update your fork's `master` branch. - -```bash -git checkout master -git pull upstream master -git push origin master -git branch -d feat/my-feature -``` - ---- - -## Reporting Bugs - -Found a bug? We appreciate you taking the time to report it. - -### Before reporting - -1. **Search existing issues** – Someone might have already reported it. -2. **Check the version** – Make sure you're using the latest release. - -### Writing a good bug report - -Include as much of the following as possible: - -```markdown -## Description - -A clear and concise description of the bug. - -## Steps to reproduce - -1. Create a project with this configuration: ... -2. Add this content file: ... -3. Run `cargo run` -4. See error: ... - -## Expected behaviour - -The site should generate with ... - -## Actual behaviour - -Instead, the following error occurs: ... - -## Environment - -- OS: Ubuntu 24.04 -- Rust version: rustc 1.80.0 -- librawssg version: v0.3.0 -- Features enabled: tera, pulldown - -## Minimal reproduction - -Link to a repository or paste minimal code that reproduces the issue. -``` - -The more detail you provide, the faster we can fix the bug. - ---- - -## Feature Requests - -Have an idea to make librawssg better? We want to hear it. - -### Writing a good feature request - -```markdown -## The problem - -I want to use custom template engines, but currently I have to -implement TemplateRenderer which is not well documented. - -## Proposed solution - -Add a guide showing how to implement TemplateRenderer for a -custom engine, with Handlebars as an example. - -## Alternatives considered - -I could use Tera, but my existing templates are in Handlebars. - -## Additional context - -Handlebars is widely used in the JavaScript ecosystem, so -supporting it would attract more users. -``` - -### What makes a good feature request - -- **Describes a real problem** – Not just "it would be cool if…" -- **Fits the project scope** – librawssg is a kernel, not a full website builder. -- **Is specific** – Vague requests are hard to act on. -- **Considers alternatives** – Shows you've thought about the problem. - -For large features, consider opening a discussion issue first to gather feedback before writing code. - ---- - -## Documentation - -Documentation is just as important as code. We follow these principles: - -### What to document - -- Every public API item (types, traits, functions) -- Configuration options -- How to get started (tutorials) -- How to contribute (this guide) - -### Where documentation lives - -| Documentation type | Location | -| ------------------ | ------------------------------- | -| API docs | `///` comments in source code | -| User guide | `docs/content/*.md` (this site) | -| README | `README.md` in the root | -| Release notes | GitHub releases | - -### Writing good documentation - -- Write in plain English. -- Use short sentences and paragraphs. -- Include code examples that can be copied and run. -- Use consistent terminology. -- Update documentation when you change behaviour. - -### Building the documentation site - -```bash -cd docs -cargo run -``` - -The generated site appears in `docs/dist/`. - ---- - -## Community - -### Communication channels - -- **GitHub Issues** – Bug reports and feature requests -- **GitHub Discussions** – Questions and general conversation -- **Pull Requests** – Code contributions and review - -### Getting help - -If you're stuck or have questions: - -1. Check the [documentation](/). -2. Search [existing issues](https://github.com/mroczect/librawssg/issues). -3. Open a new discussion or issue if you can't find an answer. - -### Etiquette - -- Be patient – maintainers are volunteers. -- Be clear – the more context you provide, the better the answer. -- Be kind – everyone was a beginner once. - ---- - -## Recognition - -All contributors are recognised in the project. When your PR is merged: - -- Your name appears in the Git history. -- You may be listed in the README (ask if you'd like to be added). -- You get the satisfaction of knowing you helped build something useful. - ---- - -Thank you for contributing to **librawssg**. Your effort makes this project better for everyone. diff --git a/docs/content/index.md b/docs/content/index.md deleted file mode 100644 index b190caf..0000000 --- a/docs/content/index.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: librawssg -desc: A safety‑first, engine‑agnostic static site generator kernel for Rust ---- - -**librawssg** is the core engine that powers `rawssg`. -It gives you complete freedom to build your own static site generator with the tools you already know. - -**Engine‑agnostic** – Bring your own template engine (Tera, Handlebars, …) and Markdown renderer (pulldown‑cmark, comrak, …) through simple Rust traits. - -**Safety‑first** – Built‑in path‑traversal protection, atomic output generation, and strict configuration validation mean you never have to worry about broken builds or security holes. - -**Batteries optional** – The library ships with zero mandatory dependencies. Enable the built‑in Tera and pulldown‑cmark implementations with a single feature flag, or plug in your own. - ---- - -## Why librawssg? - -- **You control the pipeline** – Write your own `main.rs` and compose exactly the pieces you need. -- **Test everything** – Every component is abstracted behind a trait; mock the filesystem and renderers for fast, reliable tests. -- **No magic** – Configuration is validated up‑front. Errors are typed and described with `miette` diagnostics. - ---- - -## Quick example - -```rust -use librawssg::site::builder::SiteBuilder; -use librawssg::site::TeraRenderer; -use librawssg::markdown::PulldownMarkdown; - -let site = SiteBuilder::new() - .load_config("config.yml")? - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .build()?; - -site.generate()?; -``` - ---- - -## Explore the documentation - -- [Installation](installation.html) – Add librawssg to your project -- [Configuration](configuration.html) – Full YAML reference -- [API Reference](references.html) – All public types and traits -- [Contributing](contributing.html) – Help us improve diff --git a/docs/content/installation.md b/docs/content/installation.md deleted file mode 100644 index c434f1d..0000000 --- a/docs/content/installation.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: Installation -desc: Add librawssg to your Rust project in under a minute ---- - -librawssg is **not yet published on [crates.io](https://crates.io)**, but you can still use it today directly from the Git repository. -All methods below take less than a minute – pick the one that fits your workflow. - ---- - -## Prerequisites - -- **Rust** `1.96` or later (install via [rustup](https://rustup.rs)) -- A Cargo project with `edition = "2024"` (or `2021` if you prefer) - ---- - -## Method 1: `cargo add` (Git dependency – recommended) - -If you have `cargo-edit` installed (`cargo install cargo-edit`), the fastest way is: - -```bash -cargo add --git https://github.com/mroczect/librawssg.git --tag v0.4.0 librawssg -``` - -To also enable the built‑in Tera and pulldown‑cmark implementations (most common): - -```bash -cargo add --git https://github.com/mroczect/librawssg.git --tag v0.4.0 librawssg --features tera,pulldown -``` - -This will add the dependency to your `Cargo.toml` automatically. - ---- - -## Method 2: Manual `Cargo.toml` entry - -Add this to your `Cargo.toml`: - -```toml -[dependencies] -librawssg = { git = "https://github.com/mroczect/librawssg.git", tag = "v0.4.0" } -``` - -**Always pin a specific tag** to avoid breaking changes. To enable Tera and pulldown-cmark: - -```toml -librawssg = { git = "https://github.com/mroczect/librawssg.git", tag = "v0.4.0", features = ["tera", "pulldown"] } -``` - ---- - -## Method 3: Path dependency (local development) - -If you cloned the repository and want to hack on the library: - -```bash -git clone https://github.com/mroczect/librawssg.git -cd librawssg -``` - -Then in your project’s `Cargo.toml`: - -```toml -[dependencies] -librawssg = { path = "../librawssg", features = ["tera", "pulldown"] } -``` - ---- - -## Method 4: Full clone and build - -```bash -git clone https://github.com/mroczect/librawssg.git -cd librawssg -cargo build --release -``` - -Then reference it as a path dependency (see Method 3). - ---- - -## Feature Flags - -librawssg is modular – you only pay for what you use. - -| Flag | What it enables | Adds crate(s) | -| ---------- | --------------------------- | --------------------- | -| `tera` | Built‑in `TeraRenderer` | `tera` | -| `pulldown` | Built‑in `PulldownMarkdown` | `pulldown-cmark` | -| `serve` | Dev server with live reload | `tiny_http`, `notify` | - -All features are **disabled by default**. -If you don't enable `tera` or `pulldown`, you must provide your own implementations of `TemplateRenderer` and `MarkdownRenderer`. - ---- - -## Verify your installation - -Create a small test binary to confirm everything compiles: - -```rust -// src/main.rs -use librawssg::site::builder::SiteBuilder; - -fn main() { - let _ = SiteBuilder::new(); - println!("librawssg is ready!"); -} -``` - -```bash -cargo run -``` - -If you see `"librawssg is ready!"`, the library is correctly linked. - ---- - -## Minimal working example - -Here's a complete, copy‑paste ready project that generates a real site. - -**1. Create a new binary project** - -```bash -cargo new my-site -cd my-site -``` - -**2. Add the dependency (choose one method)** - -Using `cargo add`: - -```bash -cargo add --git https://github.com/mroczect/librawssg.git --tag v0.4.0 librawssg --features tera,pulldown -``` - -Or edit `Cargo.toml` manually (see Method 2). - -**3. Create folders and a content file** - -```bash -mkdir content templates static -echo '--- -title: Hello -desc: My first page ---- -# Welcome!' > content/index.md - -echo '{{ page_content | safe }}' > templates/base.html -``` - -**4. Create `config.yml`** - -```yaml -site: - site_name: "My Site" - -build: - content_dir: "content" - output_dir: "dist" - templates_dir: "templates" - static_dir: "static" - -content_types: - - name: "page" - pattern: "*.md" - template: "base.html" - list_enabled: false -``` - -**5. Replace `src/main.rs`** - -```rust -use librawssg::markdown::PulldownMarkdown; -use librawssg::site::builder::SiteBuilder; -use librawssg::site::TeraRenderer; -use std::path::Path; - -fn main() -> Result<(), Box> { - let mut tera = TeraRenderer::new(); - let templates_dir = Path::new("templates"); - for entry in std::fs::read_dir(templates_dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_file() { - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - let content = std::fs::read_to_string(&path)?; - tera.add_raw_template(&name, &content)?; - } - } - - let site = SiteBuilder::new() - .load_config("config.yml")? - .content_dir("content") - .output_dir("dist") - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(PulldownMarkdown)) - .build()?; - - site.generate()?; - println!("Site generated in dist/"); - Ok(()) -} -``` - -**6. Run it** - -```bash -cargo run -open dist/index.html -``` - -You should see a simple HTML page with "Welcome!" rendered from Markdown. - ---- - -## Troubleshooting - -### `error: failed to parse manifest` when using a tag - -Make sure the tag exists on GitHub (e.g., `v0.4.0`). You can also use `branch = "master"` temporarily, but **prefer a tag for stability**. - -### `cannot find trait implementation` for Tera or Pulldown - -You forgot to enable the `tera` and `pulldown` features. Add them to your `Cargo.toml` or `cargo add` command. - -### `No context implementation available` error at runtime - -You tried to build a site without a template renderer. Make sure you call `.with_template_renderer(...)` before `.build()`. - -### `File not found` when running from a different directory - -The `content_dir`, `templates_dir`, and `static_dir` paths are relative to the current working directory. Run the binary from the project root, or use absolute paths. - ---- - -## Next steps - -- [Configuration](configuration.html) – Learn every YAML option -- [API Reference](references.html) – Discover all traits and types -- [Demo project](https://github.com/mroczect/librawssg/tree/master/demo) – A full‑featured example with blog, RSS, and sitemap \ No newline at end of file diff --git a/docs/content/license.md b/docs/content/license.md deleted file mode 100644 index 067c86e..0000000 --- a/docs/content/license.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: License -desc: MIT License ---- - -```txt -The MIT License (MIT) - -Copyright (c) 2026 mroczect - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -``` diff --git a/docs/content/references.md b/docs/content/references.md deleted file mode 100644 index abb1346..0000000 --- a/docs/content/references.md +++ /dev/null @@ -1,538 +0,0 @@ ---- -title: API Reference -desc: Complete reference of all public types, traits, functions, and modules in librawssg v0.5.0 ---- - -This document lists every public item exported by `librawssg`. -Items gated behind Cargo features are clearly marked. - ---- - -## `config` module - -Configuration loading traits and implementations. - -### `ConfigLoader` trait - -```rust -pub trait ConfigLoader: Send + Sync { - fn load(&self) -> Result; - fn load_or_default(&self) -> RawssgConfig { - self.load().unwrap_or_else(|e| { - tracing::error!("Failed to load config, using defaults: {}", e); - RawssgConfig::default() - }) - } -} -``` - -Implementors produce a `RawssgConfig`. `load_or_default()` logs the error and returns the default configuration if loading fails. - -### `YamlConfigLoader

` - -```rust -pub struct YamlConfigLoader + Send + Sync> { /* path */ } -``` - -Reads a YAML file and deserialises it into a `RawssgConfig`. -**Constructor**: - -```rust -pub fn new(path: P) -> Self; -``` - -**Trait implementation**: `ConfigLoader`. - -### `DefaultConfig` - -```rust -pub struct DefaultConfig; -``` - -A `ConfigLoader` that always returns `RawssgConfig::default()` (which now includes a default `page` content type). - ---- - -## `error` module - -### `RawssgError` enum - -```rust -pub enum RawssgError { - Io(std::io::Error), - Config(String), - Frontmatter { path: PathBuf, source: Box }, - Template(String), - PathTraversal(String), - MissingConfig(String), - Markdown(String), - SiteGeneration(String), - NotFound(String), - Internal(String), -} -``` - -All errors implement `Display` and `Diagnostic` (via `miette`). -Variants are used throughout the library for specific failure conditions. - ---- - -## `frontmatter` module - -### `parse_frontmatter_and_render` - -```rust -pub fn parse_frontmatter_and_render( - raw: &str, - path: &Path, - renderer: &dyn MarkdownRenderer, -) -> Result<(PageFrontMatter, String), RawssgError>; -``` - -Extracts YAML frontmatter from a string. Returns a `Frontmatter` error if the opening or closing `---` is missing, or if the YAML is invalid. -Renders the remaining Markdown to HTML using the provided renderer. Returns the frontmatter and the rendered HTML. - ---- - -## `fs` module - -### `FileSystem` trait - -```rust -pub trait FileSystem: Send + Sync { - fn read_to_string(&self, path: &Path) -> io::Result; - fn read_bytes(&self, path: &Path) -> io::Result>; - fn write(&self, path: &Path, content: &[u8]) -> io::Result<()>; - fn create_dir_all(&self, path: &Path) -> io::Result<()>; - fn remove_dir_all(&self, path: &Path) -> io::Result<()>; - fn exists(&self, path: &Path) -> bool; - fn is_dir(&self, path: &Path) -> bool; - fn is_file(&self, path: &Path) -> bool; - fn read_dir(&self, path: &Path) -> io::Result>; - fn copy_file(&self, from: &Path, to: &Path) -> io::Result; - fn walk_dir(&self, root: &Path) -> io::Result>; - fn canonicalize(&self, path: &Path) -> io::Result; - fn rename(&self, from: &Path, to: &Path) -> io::Result<()>; // new in v0.5.0 -} -``` - -Abstraction over all filesystem operations. Implement this trait to support in‑memory files, network storage, or any custom backend. -The `rename` method is used by `Site::generate` for atomic output; a cross‑device fallback is triggered if `rename` returns `CrossesDevices`. - -### `RealFs` - -```rust -pub struct RealFs; -``` - -Implements `FileSystem` by delegating to `std::fs` and `walkdir`. All methods are instrumented with `tracing`. - ---- - -## `markdown` module - -### `MarkdownRenderer` trait - -```rust -pub trait MarkdownRenderer: Send + Sync { - fn render(&self, markdown: &str) -> String; -} -``` - -Converts Markdown text to HTML. - -### `PulldownMarkdown` [![feature: pulldown](https://img.shields.io/badge/feature-pulldown-blue)] - -```rust -pub struct PulldownMarkdown; -``` - -Available with the `pulldown` feature. -Implements `MarkdownRenderer` using `pulldown‑cmark` with tables, strikethrough, and tasklists enabled. - ---- - -## `serve` module [![feature: serve](https://img.shields.io/badge/feature-serve-blue)] - -### `start_dev_server` - -```rust -pub fn start_dev_server(output_dir: &Path, port: u16) -> Result<(), RawssgError>; -``` - -Starts a simple HTTP server that serves static files from `output_dir`. -Requests are handled in separate threads. MIME types are detected from file extensions (including `text/plain` for `.txt`, `.md`, `.yaml`, `.yml`, `.log`). Uses `safe_path` to prevent directory traversal. - -### `watch_dirs` - -```rust -pub fn watch_dirs(dirs: &[PathBuf], on_change: F) -> notify::Result> -where - F: Fn() + Send + 'static; -``` - -Watches one or more directories for changes (modify, create, remove) and calls `on_change` on each event. Returns a watcher that must be kept alive. - ---- - -## `site` module - -### `TemplateRenderer` trait - -```rust -pub trait TemplateRenderer: Send + Sync { - fn render(&self, template_name: &str, context: &dyn Context) -> Result; -} -``` - -Renders a named template with a given context. The context is engine‑specific and accessed via the `Context` trait. - -### `Context` trait - -```rust -pub trait Context: Send + Sync { - fn as_any(&self) -> &dyn std::any::Any; - fn as_mut_any(&mut self) -> &mut dyn std::any::Any; -} -``` - -Enables type‑erased access to the underlying template context. Engine implementations must implement this trait for their context type. - -### `TeraRenderer` [![feature: tera](https://img.shields.io/badge/feature-tera-blue)] - -```rust -pub struct TeraRenderer { /* contains a tera::Tera */ } -``` - -Available with the `tera` feature. -**Constructor**: - -```rust -pub fn new() -> Self; -``` - -**Methods**: - -```rust -pub fn add_raw_template(&mut self, name: &str, content: &str) -> Result<(), RawssgError>; -``` - -Adds a template to the engine. Call this before passing the renderer to `SiteBuilder`. -`TeraRenderer` implements `TemplateRenderer` and `Default`. - -### `ContentHandler` trait - -```rust -pub trait ContentHandler: Send + Sync { - fn can_handle(&self, relative_path: &Path, original_path: &Path) -> bool; - fn process( - &self, - fs: &dyn FileSystem, - md_renderer: &dyn MarkdownRenderer, - file_path: &Path, - content_dir: &Path, - ) -> Result, RawssgError>; -} -``` - -Processes a file into a `PageContext`. Return `None` to skip the file (e.g., drafts). - -### `MarkdownPageHandler` - -```rust -pub struct MarkdownPageHandler; -``` - -Handles `.md` files by parsing frontmatter and rendering Markdown. Implements `ContentHandler`. - -### `StaticFileHandler` - -```rust -pub struct StaticFileHandler; -``` - -A catch‑all handler that always returns `None`. It is included by default to prevent warnings for non‑Markdown files (which are copied as assets separately). Implements `ContentHandler`. - -### `build_page_context` - -```rust -pub fn build_page_context( - fs: &dyn FileSystem, - markdown_renderer: &dyn MarkdownRenderer, - file_path: &Path, - content_dir: &Path, -) -> Result, RawssgError>; -``` - -Validates the path, reads the file, extracts frontmatter, renders Markdown, and returns a `PageContext`. Drafts are skipped automatically. -This function is used internally but is public for custom handler implementations. - ---- - -### `SiteBuilder` - -```rust -pub struct SiteBuilder { /* private fields */ } -``` - -The entry point for constructing a site. - -**Location**: `librawssg::site::builders::site_builder` (re‑exported as `librawssg::SiteBuilder`). - -**Methods**: - -| Method | Description | -| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `pub fn new() -> Self` | Creates a new builder with default values and `RealFs`. | -| `pub fn config(self, config: RawssgConfig) -> Self` | Sets the entire configuration. | -| `pub fn load_config + Send + Sync>(self, path: P) -> Result` | Loads configuration from a YAML file. | -| `pub fn content_dir(self, dir: impl Into) -> Self` | Sets the content directory (default `"content"`). | -| `pub fn output_dir(self, dir: impl Into) -> Self` | Sets the output directory (default `"dist"`). | -| `pub fn with_fs(self, fs: Box) -> Self` | Sets the filesystem backend. | -| `pub fn with_markdown_renderer(self, md: Box) -> Self` | Sets the Markdown renderer (**required**). | -| `pub fn with_template_renderer(self, tr: Box) -> Self` | Sets the template renderer (**required**). | -| `pub fn add_handler(self, handler: Box) -> Self` | Adds a content handler (defaults: `MarkdownPageHandler`, `StaticFileHandler`). | -| `pub fn with_feed_context_builder(self, b: Box) -> Self` | Sets the feed context builder. **Only required if `generators.rss.enabled` is `true`.** [![feature: tera](https://img.shields.io/badge/feature-tera-blue)] | -| `pub fn with_sitemap_context_builder(self, b: Box) -> Self` | Sets the sitemap context builder. **Only required if `generators.sitemap.enabled` is `true`.** [![feature: tera](https://img.shields.io/badge/feature-tera-blue)] | -| `pub fn build(self) -> Result` | Validates config, canonicalizes directories, processes all content, and returns a ready‑to‑generate `Site`. | - -### `Site` - -```rust -pub struct Site { /* private fields */ } -``` - -A fully processed site ready for output generation. -**Location**: `librawssg::site::builders::site` (re‑exported as `librawssg::Site`). - -**Methods**: - -| Method | Description | -| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `pub fn pages(&self) -> &[PageContext]` | Returns all pages (including list pages). | -| `pub fn generate(self) -> Result<(), RawssgError>` | Writes HTML files, copies static assets and non‑Markdown files, optionally generates RSS and sitemap. Uses atomic output via `FileSystem::rename` with a recursive copy fallback if the rename fails due to `CrossesDevices`. | - ---- - -### `generate_feed` [![feature: tera](https://img.shields.io/badge/feature-tera-blue)] - -```rust -pub fn generate_feed( - renderer: &dyn TemplateRenderer, - config: &RawssgConfig, - posts: &[&PageContext], - base_url: &str, - context_builder: &dyn FeedContextBuilder, -) -> Result; -``` - -Renders an RSS feed from a list of blog posts using the configured RSS template and the provided context builder. - -### `generate_sitemap` [![feature: tera](https://img.shields.io/badge/feature-tera-blue)] - -```rust -pub fn generate_sitemap( - renderer: &dyn TemplateRenderer, - config: &RawssgConfig, - pages: &[PageContext], - base_url: &str, - context_builder: &dyn SitemapContextBuilder, -) -> Result; -``` - -Renders a sitemap XML from all pages using the configured sitemap template and the provided context builder. - -### Context builders (in `site::context`) [![feature: tera](https://img.shields.io/badge/feature-tera-blue)] - -```rust -pub trait FeedContextBuilder: Send + Sync { - fn build_feed_context(&self, config: &RawssgConfig, posts: &[&PageContext], base_url: &str) - -> Result, RawssgError>; -} - -pub trait SitemapContextBuilder: Send + Sync { - fn build_sitemap_context(&self, config: &RawssgConfig, pages: &[PageContext], base_url: &str) - -> Result, RawssgError>; -} -``` - -Default Tera implementations (`TeraFeedContextBuilder`, `TeraSitemapContextBuilder`) insert `site`, `posts`/`pages`, and `base_url`. Custom builders can inject additional variables. - ---- - -## `types` module - -### `RawssgConfig` - -```rust -pub struct RawssgConfig { - pub site: GlobalConfig, - pub build: BuildConfig, - pub content_types: Vec, - pub generators: GeneratorsConfig, -} -``` - -Top‑level configuration. -**Default**: includes a `page` content type (`**/*.md` → `base.html`). -**Method**: - -```rust -pub fn validate(&self) -> Result<(), RawssgError>; -``` - -Validates that `site_name` is non‑empty, `content_types` is non‑empty, each content type has a valid glob pattern and template, and required generator fields are present when enabled. - -### `GlobalConfig` - -```rust -pub struct GlobalConfig { - pub navbar: Vec, - pub sidebar: Vec, - pub site_name: String, // default "rawssg" - pub description: Option, - pub language: Option, // default Some("en") - pub base_url: Option, - pub author: Option, - pub repo_url: Option, - pub license: Option, -} -``` - -Site‑wide metadata available in templates as `site.`. - -### `BuildConfig` - -```rust -pub struct BuildConfig { - pub content_dir: String, // default "content" - pub output_dir: String, // default "dist" - pub templates_dir: String, // default "templates" - pub static_dir: String, // default "static" -} -``` - -### `ContentTypeDef` - -```rust -pub struct ContentTypeDef { - pub name: String, - pub pattern: String, - pub template: String, - pub list_template: Option, - pub list_enabled: bool, -} -``` - -Defines a content type. `pattern` is a glob relative to `content_dir`. If `list_enabled` is `true`, a list page (`{name}/index.html`) is generated using `list_template`. - -### `GeneratorsConfig` - -```rust -pub struct GeneratorsConfig { - pub rss: GeneratorDef, - pub sitemap: GeneratorDef, -} -``` - -### `GeneratorDef` - -```rust -pub struct GeneratorDef { - pub enabled: bool, // default false (changed in v0.5.0) - pub path: String, - pub template: String, -} -``` - -Configuration for RSS and sitemap generators. Both default to **disabled**. When enabled, `path` and `template` must be provided. - -### `NavItem` - -```rust -pub struct NavItem { - pub label: String, - pub url: String, -} -``` - -Used in `navbar` and `sidebar`. - -### `PageFrontMatter` - -```rust -pub struct PageFrontMatter { - pub title: String, - pub desc: String, - pub author: Option, - pub repo_url: Option, - pub license: Option, - pub date: Option, - pub tags: Vec, - pub draft: bool, -} -``` - -Parsed from the YAML frontmatter block. `title` and `desc` are required; everything else is optional. - -### `PageContext` - -```rust -pub struct PageContext { - pub frontmatter: PageFrontMatter, - pub content_html: String, - pub url: String, - pub file_path: String, - pub depth: usize, - pub pub_date: Option, - pub content_type: String, - pub is_list: bool, - pub list_items: Option>, -} -``` - -Fully processed representation of a page. `pub_date` is an RFC 2822 string if `date` was present in the frontmatter. `list_items` is populated only for list pages. - ---- - -## `util` module - -### `safe_path` - -```rust -pub fn safe_path( - fs: &dyn FileSystem, - base: &Path, - candidate: &Path, -) -> Result; -``` - -Validates that `candidate` is inside `base`. -- If the candidate exists, it canonicalises the path and checks the prefix. -- If the candidate does not exist (used for output files), it canonicalises the parent directory and verifies it is still within `base`. -Prevents directory traversal and symlink escapes. Returns the safe canonical path. - -### `slugify` - -```rust -pub fn slugify(title: &str) -> String; -``` - -Converts a string to a URL‑friendly slug (lowercase, only alphanumerics and hyphens). - -### `relative_prefix` - -```rust -pub fn relative_prefix(depth: usize) -> String; -``` - -Returns a relative path prefix based on directory depth: `"./"` for depth 0, `"../"` for depth 1, etc. - -### `match_pattern` - -```rust -pub fn match_pattern(pattern: &str, path: &Path) -> bool; -``` - -Matches a path against a glob pattern with `*` (single segment) and `**` (any number of segments). -The algorithm correctly handles patterns like `blog/**/*.html` (will not match `.md`). Used internally for content type matching. \ No newline at end of file diff --git a/docs/src/main.rs b/docs/src/main.rs deleted file mode 100644 index ee4c776..0000000 --- a/docs/src/main.rs +++ /dev/null @@ -1,49 +0,0 @@ -use librawssg::markdown::PulldownMarkdown; -use librawssg::site::context::{TeraFeedContextBuilder, TeraSitemapContextBuilder}; -use librawssg::site::TeraRenderer; -use librawssg::SiteBuilder; -use std::path::Path; - -fn main() -> Result<(), Box> { - let builder = SiteBuilder::new() - .load_config("config.yml") - .expect("Failed to load config.yml"); - - let mut tera = TeraRenderer::new(); - let templates_dir = Path::new("templates"); - if templates_dir.exists() { - for entry in std::fs::read_dir(templates_dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_file() { - let name = path.file_name().unwrap().to_string_lossy().into_owned(); - let content = std::fs::read_to_string(&path)?; - tera.add_raw_template(&name, &content)?; - } - } - } - - let md = PulldownMarkdown; - - let content_dir = Path::new("content"); - let output_dir = Path::new("dist"); - - let site = builder - .content_dir(content_dir) - .output_dir(output_dir) - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .with_feed_context_builder(Box::new(TeraFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(TeraSitemapContextBuilder)) - .build()?; - - println!("Generated {} pages:", site.pages().len()); - for page in site.pages() { - println!(" {} -> {}", page.file_path, page.url); - } - - site.generate()?; - println!("\nDocumentation built successfully in {}", output_dir.display()); - - Ok(()) -} \ No newline at end of file diff --git a/docs/static/normalize.css b/docs/static/normalize.css deleted file mode 100644 index bb6e2a7..0000000 --- a/docs/static/normalize.css +++ /dev/null @@ -1,351 +0,0 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ - -/* Document - ========================================================================== */ - -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ - -html { - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers. - */ - -body { - margin: 0; -} - -/** - * Render the `main` element consistently in IE. - */ - -main { - display: block; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Remove the gray background on active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10. - */ - -img { - border-style: none; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { - /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { - /* 1 */ - text-transform: none; -} - -/** - * Correct the inability to style clickable types in iOS and Safari. - */ - -button, -[type="button"], -[type="reset"], -[type="submit"] { - -webkit-appearance: button; -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Correct the padding in Firefox. - */ - -fieldset { - padding: 0.35em 0.75em 0.625em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Remove the default vertical scrollbar in IE 10+. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ - -details { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Misc - ========================================================================== */ - -/** - * Add the correct display in IE 10+. - */ - -template { - display: none; -} - -/** - * Add the correct display in IE 10. - */ - -[hidden] { - display: none; -} diff --git a/docs/static/sidebar.css b/docs/static/sidebar.css deleted file mode 100644 index 2f58bd9..0000000 --- a/docs/static/sidebar.css +++ /dev/null @@ -1,88 +0,0 @@ -/* Layout dasar */ -.wrapper { - display: flex; - min-height: 100vh; -} - -/* Sidebar – sangat sederhana */ -.sidebar { - width: 220px; - flex-shrink: 0; - position: sticky; - top: 0; - height: 100vh; - padding: 1.5rem 1rem; - background: #fff; - border-right: 1px solid #ccc; - overflow-y: auto; -} - -.sidebar h2 { - font-size: 1rem; - font-weight: 600; - margin: 0 0 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #ddd; -} - -.sidebar h2 a { - color: #000; - text-decoration: none; -} - -.sidebar ul { - list-style: none; - padding: 0; - margin: 0; -} - -.sidebar li { - margin-bottom: 0.15rem; -} - -.sidebar a { - display: block; - padding: 0.3rem 0.5rem; - color: #333; - text-decoration: none; - font-size: 0.9rem; -} - -/* Tidak ada hover background, hanya underline */ -.sidebar a:hover { - text-decoration: underline; - color: #000; -} - -.sidebar a.active { - font-weight: 700; - color: #000; - text-decoration: underline; -} - -/* Konten utama – polos */ -.content { - flex: 1; - padding: 1.5rem 2rem; - background: #fff; -} - -/* Responsif – tetap berfungsi */ -@media (max-width: 768px) { - .wrapper { - flex-direction: column; - } - - .sidebar { - width: 100%; - height: auto; - position: static; - border-right: none; - border-bottom: 1px solid #ccc; - padding: 1rem; - } - - .content { - padding: 1rem; - } -} \ No newline at end of file diff --git a/docs/static/style.css b/docs/static/style.css deleted file mode 100644 index a964b5e..0000000 --- a/docs/static/style.css +++ /dev/null @@ -1,6838 +0,0 @@ -/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ -html { - font-family: sans-serif; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; -} - -body { - margin: 0; -} - -article, -aside, -details, -figcaption, -figure, -footer, -header, -main, -menu, -nav, -section { - display: block; -} - -summary { - display: list-item; -} - -audio, -canvas, -progress, -video { - display: inline-block; -} - -audio:not([controls]) { - display: none; - height: 0; -} - -progress { - vertical-align: baseline; -} - -template, -[hidden] { - display: none !important; -} - -a { - background-color: transparent; -} - -a:active, -a:hover { - outline-width: 0; -} - -abbr[title] { - border-bottom: none; - text-decoration: underline; - text-decoration: underline dotted; -} - -b, -strong { - font-weight: inherit; -} - -b, -strong { - font-weight: bolder; -} - -dfn { - font-style: italic; -} - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -mark { - background-color: #ff0; - color: #000; -} - -small { - font-size: 80%; -} - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -img { - border-style: none; -} - -svg:not(:root) { - overflow: hidden; -} - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -figure { - margin: 1em 40px; -} - -hr { - box-sizing: content-box; - height: 0; - overflow: visible; -} - -button, -input, -select, -textarea { - font: inherit; - margin: 0; -} - -optgroup { - font-weight: bold; -} - -button, -input { - overflow: visible; -} - -button, -select { - text-transform: none; -} - -button, -html [type="button"], -[type="reset"], -[type="submit"] { - -webkit-appearance: button; -} - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -legend { - box-sizing: border-box; - color: inherit; - display: table; - max-width: 100%; - padding: 0; - white-space: normal; -} - -textarea { - overflow: auto; -} - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; - padding: 0; -} - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -[type="search"] { - -webkit-appearance: textfield; - outline-offset: -2px; -} - -[type="search"]::-webkit-search-cancel-button, -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; -} - -::-webkit-file-upload-button { - -webkit-appearance: button; - font: inherit; -} - -* { - box-sizing: border-box; -} - -input, -select, -textarea, -button { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - font-size: 14px; - line-height: 1.5; - color: #24292e; - background-color: #fff; -} - -a { - color: #0366d6; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -b, -strong { - font-weight: 600; -} - -hr, -.rule { - height: 0; - margin: 15px 0; - overflow: hidden; - background: transparent; - border: 0; - border-bottom: 1px solid #dfe2e5; -} - -hr::before, -.rule::before { - display: table; - content: ""; -} - -hr::after, -.rule::after { - display: table; - clear: both; - content: ""; -} - -table { - border-spacing: 0; - border-collapse: collapse; -} - -td, -th { - padding: 0; -} - -button { - cursor: pointer; - border-radius: 0; -} - -[hidden][hidden] { - display: none !important; -} - -details summary { - cursor: pointer; -} - -details:not([open]) > *:not(summary) { - display: none !important; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - margin-top: 0; - margin-bottom: 0; -} - -h1 { - font-size: 32px; - font-weight: 600; -} - -h2 { - font-size: 24px; - font-weight: 600; -} - -h3 { - font-size: 20px; - font-weight: 600; -} - -h4 { - font-size: 16px; - font-weight: 600; -} - -h5 { - font-size: 14px; - font-weight: 600; -} - -h6 { - font-size: 12px; - font-weight: 600; -} - -p { - margin-top: 0; - margin-bottom: 10px; -} - -small { - font-size: 90%; -} - -blockquote { - margin: 0; -} - -ul, -ol { - padding-left: 0; - margin-top: 0; - margin-bottom: 0; -} - -ol ol, -ul ol { - list-style-type: lower-roman; -} - -ul ul ol, -ul ol ol, -ol ul ol, -ol ol ol { - list-style-type: lower-alpha; -} - -dd { - margin-left: 0; -} - -tt, -code { - font-family: - "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 12px; -} - -pre { - margin-top: 0; - margin-bottom: 0; - font-family: - "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 12px; -} - -.octicon { - vertical-align: text-bottom; -} - -.anim-fade-in { - animation-name: fade-in; - animation-duration: 1s; - animation-timing-function: ease-in-out; -} - -.anim-fade-in.fast { - animation-duration: 300ms; -} - -@keyframes fade-in { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -.anim-fade-out { - animation-name: fade-out; - animation-duration: 1s; - animation-timing-function: ease-out; -} - -.anim-fade-out.fast { - animation-duration: 0.3s; -} - -@keyframes fade-out { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -.anim-fade-up { - opacity: 0; - animation-name: fade-up; - animation-duration: 0.3s; - animation-fill-mode: forwards; - animation-timing-function: ease-out; - animation-delay: 1s; -} - -@keyframes fade-up { - 0% { - opacity: 0.8; - transform: translateY(100%); - } - - 100% { - opacity: 1; - transform: translateY(0); - } -} - -.anim-fade-down { - animation-name: fade-down; - animation-duration: 0.3s; - animation-fill-mode: forwards; - animation-timing-function: ease-in; -} - -@keyframes fade-down { - 0% { - opacity: 1; - transform: translateY(0); - } - - 100% { - opacity: 0.5; - transform: translateY(100%); - } -} - -.anim-grow-x { - width: 0%; - animation-name: grow-x; - animation-duration: 0.3s; - animation-fill-mode: forwards; - animation-timing-function: ease; - animation-delay: 0.5s; -} - -@keyframes grow-x { - to { - width: 100%; - } -} - -.anim-shrink-x { - animation-name: shrink-x; - animation-duration: 0.3s; - animation-fill-mode: forwards; - animation-timing-function: ease-in-out; - animation-delay: 0.5s; -} - -@keyframes shrink-x { - to { - width: 0%; - } -} - -.anim-scale-in { - animation-name: scale-in; - animation-duration: 0.15s; - animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1.5); -} - -@keyframes scale-in { - 0% { - opacity: 0; - transform: scale(0.5); - } - - 100% { - opacity: 1; - transform: scale(1); - } -} - -.anim-pulse { - animation-name: pulse; - animation-duration: 2s; - animation-timing-function: linear; - animation-iteration-count: infinite; -} - -@keyframes pulse { - 0% { - opacity: 0.3; - } - - 10% { - opacity: 1; - } - - 100% { - opacity: 0.3; - } -} - -.anim-pulse-in { - animation-name: pulse-in; - animation-duration: 0.5s; -} - -@keyframes pulse-in { - 0% { - transform: scale3d(1, 1, 1); - } - - 50% { - transform: scale3d(1.1, 1.1, 1.1); - } - - 100% { - transform: scale3d(1, 1, 1); - } -} - -.hover-grow { - transition: transform 0.3s; - backface-visibility: hidden; -} - -.hover-grow:hover { - transform: scale(1.025); -} - -.border { - border: 1px #e1e4e8 solid !important; -} - -.border-y { - border-top: 1px #e1e4e8 solid !important; - border-bottom: 1px #e1e4e8 solid !important; -} - -.border-0 { - border: 0 !important; -} - -.border-dashed { - border-style: dashed !important; -} - -.border-blue { - border-color: #0366d6 !important; -} - -.border-blue-light { - border-color: #c8e1ff !important; -} - -.border-green { - border-color: #34d058 !important; -} - -.border-green-light { - border-color: #a2cbac !important; -} - -.border-red { - border-color: #d73a49 !important; -} - -.border-red-light { - border-color: #cea0a5 !important; -} - -.border-purple { - border-color: #6f42c1 !important; -} - -.border-yellow { - border-color: #d9d0a5 !important; -} - -.border-gray-light { - border-color: #eaecef !important; -} - -.border-gray-dark { - border-color: #d1d5da !important; -} - -.border-black-fade { - border-color: rgba(27, 31, 35, 0.15) !important; -} - -.border-top { - border-top: 1px #e1e4e8 solid !important; -} - -.border-right { - border-right: 1px #e1e4e8 solid !important; -} - -.border-bottom { - border-bottom: 1px #e1e4e8 solid !important; -} - -.border-left { - border-left: 1px #e1e4e8 solid !important; -} - -.border-top-0 { - border-top: 0 !important; -} - -.border-right-0 { - border-right: 0 !important; -} - -.border-bottom-0 { - border-bottom: 0 !important; -} - -.border-left-0 { - border-left: 0 !important; -} - -.rounded-0 { - border-radius: 0 !important; -} - -.rounded-1 { - border-radius: 3px !important; -} - -.rounded-2 { - border-radius: 6px !important; -} - -.rounded-top-0 { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; -} - -.rounded-top-1 { - border-top-left-radius: 3px !important; - border-top-right-radius: 3px !important; -} - -.rounded-top-2 { - border-top-left-radius: 6px !important; - border-top-right-radius: 6px !important; -} - -.rounded-right-0 { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - -.rounded-right-1 { - border-top-right-radius: 3px !important; - border-bottom-right-radius: 3px !important; -} - -.rounded-right-2 { - border-top-right-radius: 6px !important; - border-bottom-right-radius: 6px !important; -} - -.rounded-bottom-0 { - border-bottom-right-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} - -.rounded-bottom-1 { - border-bottom-right-radius: 3px !important; - border-bottom-left-radius: 3px !important; -} - -.rounded-bottom-2 { - border-bottom-right-radius: 6px !important; - border-bottom-left-radius: 6px !important; -} - -.rounded-left-0 { - border-bottom-left-radius: 0 !important; - border-top-left-radius: 0 !important; -} - -.rounded-left-1 { - border-bottom-left-radius: 3px !important; - border-top-left-radius: 3px !important; -} - -.rounded-left-2 { - border-bottom-left-radius: 6px !important; - border-top-left-radius: 6px !important; -} - -@media (min-width: 544px) { - .border-sm-top { - border-top: 1px #e1e4e8 solid !important; - } - - .border-sm-right { - border-right: 1px #e1e4e8 solid !important; - } - - .border-sm-bottom { - border-bottom: 1px #e1e4e8 solid !important; - } - - .border-sm-left { - border-left: 1px #e1e4e8 solid !important; - } - - .border-sm-top-0 { - border-top: 0 !important; - } - - .border-sm-right-0 { - border-right: 0 !important; - } - - .border-sm-bottom-0 { - border-bottom: 0 !important; - } - - .border-sm-left-0 { - border-left: 0 !important; - } - - .rounded-sm-0 { - border-radius: 0 !important; - } - - .rounded-sm-1 { - border-radius: 3px !important; - } - - .rounded-sm-2 { - border-radius: 6px !important; - } - - .rounded-sm-top-0 { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; - } - - .rounded-sm-top-1 { - border-top-left-radius: 3px !important; - border-top-right-radius: 3px !important; - } - - .rounded-sm-top-2 { - border-top-left-radius: 6px !important; - border-top-right-radius: 6px !important; - } - - .rounded-sm-right-0 { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - } - - .rounded-sm-right-1 { - border-top-right-radius: 3px !important; - border-bottom-right-radius: 3px !important; - } - - .rounded-sm-right-2 { - border-top-right-radius: 6px !important; - border-bottom-right-radius: 6px !important; - } - - .rounded-sm-bottom-0 { - border-bottom-right-radius: 0 !important; - border-bottom-left-radius: 0 !important; - } - - .rounded-sm-bottom-1 { - border-bottom-right-radius: 3px !important; - border-bottom-left-radius: 3px !important; - } - - .rounded-sm-bottom-2 { - border-bottom-right-radius: 6px !important; - border-bottom-left-radius: 6px !important; - } - - .rounded-sm-left-0 { - border-bottom-left-radius: 0 !important; - border-top-left-radius: 0 !important; - } - - .rounded-sm-left-1 { - border-bottom-left-radius: 3px !important; - border-top-left-radius: 3px !important; - } - - .rounded-sm-left-2 { - border-bottom-left-radius: 6px !important; - border-top-left-radius: 6px !important; - } -} - -@media (min-width: 768px) { - .border-md-top { - border-top: 1px #e1e4e8 solid !important; - } - - .border-md-right { - border-right: 1px #e1e4e8 solid !important; - } - - .border-md-bottom { - border-bottom: 1px #e1e4e8 solid !important; - } - - .border-md-left { - border-left: 1px #e1e4e8 solid !important; - } - - .border-md-top-0 { - border-top: 0 !important; - } - - .border-md-right-0 { - border-right: 0 !important; - } - - .border-md-bottom-0 { - border-bottom: 0 !important; - } - - .border-md-left-0 { - border-left: 0 !important; - } - - .rounded-md-0 { - border-radius: 0 !important; - } - - .rounded-md-1 { - border-radius: 3px !important; - } - - .rounded-md-2 { - border-radius: 6px !important; - } - - .rounded-md-top-0 { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; - } - - .rounded-md-top-1 { - border-top-left-radius: 3px !important; - border-top-right-radius: 3px !important; - } - - .rounded-md-top-2 { - border-top-left-radius: 6px !important; - border-top-right-radius: 6px !important; - } - - .rounded-md-right-0 { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - } - - .rounded-md-right-1 { - border-top-right-radius: 3px !important; - border-bottom-right-radius: 3px !important; - } - - .rounded-md-right-2 { - border-top-right-radius: 6px !important; - border-bottom-right-radius: 6px !important; - } - - .rounded-md-bottom-0 { - border-bottom-right-radius: 0 !important; - border-bottom-left-radius: 0 !important; - } - - .rounded-md-bottom-1 { - border-bottom-right-radius: 3px !important; - border-bottom-left-radius: 3px !important; - } - - .rounded-md-bottom-2 { - border-bottom-right-radius: 6px !important; - border-bottom-left-radius: 6px !important; - } - - .rounded-md-left-0 { - border-bottom-left-radius: 0 !important; - border-top-left-radius: 0 !important; - } - - .rounded-md-left-1 { - border-bottom-left-radius: 3px !important; - border-top-left-radius: 3px !important; - } - - .rounded-md-left-2 { - border-bottom-left-radius: 6px !important; - border-top-left-radius: 6px !important; - } -} - -@media (min-width: 1012px) { - .border-lg-top { - border-top: 1px #e1e4e8 solid !important; - } - - .border-lg-right { - border-right: 1px #e1e4e8 solid !important; - } - - .border-lg-bottom { - border-bottom: 1px #e1e4e8 solid !important; - } - - .border-lg-left { - border-left: 1px #e1e4e8 solid !important; - } - - .border-lg-top-0 { - border-top: 0 !important; - } - - .border-lg-right-0 { - border-right: 0 !important; - } - - .border-lg-bottom-0 { - border-bottom: 0 !important; - } - - .border-lg-left-0 { - border-left: 0 !important; - } - - .rounded-lg-0 { - border-radius: 0 !important; - } - - .rounded-lg-1 { - border-radius: 3px !important; - } - - .rounded-lg-2 { - border-radius: 6px !important; - } - - .rounded-lg-top-0 { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; - } - - .rounded-lg-top-1 { - border-top-left-radius: 3px !important; - border-top-right-radius: 3px !important; - } - - .rounded-lg-top-2 { - border-top-left-radius: 6px !important; - border-top-right-radius: 6px !important; - } - - .rounded-lg-right-0 { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - } - - .rounded-lg-right-1 { - border-top-right-radius: 3px !important; - border-bottom-right-radius: 3px !important; - } - - .rounded-lg-right-2 { - border-top-right-radius: 6px !important; - border-bottom-right-radius: 6px !important; - } - - .rounded-lg-bottom-0 { - border-bottom-right-radius: 0 !important; - border-bottom-left-radius: 0 !important; - } - - .rounded-lg-bottom-1 { - border-bottom-right-radius: 3px !important; - border-bottom-left-radius: 3px !important; - } - - .rounded-lg-bottom-2 { - border-bottom-right-radius: 6px !important; - border-bottom-left-radius: 6px !important; - } - - .rounded-lg-left-0 { - border-bottom-left-radius: 0 !important; - border-top-left-radius: 0 !important; - } - - .rounded-lg-left-1 { - border-bottom-left-radius: 3px !important; - border-top-left-radius: 3px !important; - } - - .rounded-lg-left-2 { - border-bottom-left-radius: 6px !important; - border-top-left-radius: 6px !important; - } -} - -@media (min-width: 1280px) { - .border-xl-top { - border-top: 1px #e1e4e8 solid !important; - } - - .border-xl-right { - border-right: 1px #e1e4e8 solid !important; - } - - .border-xl-bottom { - border-bottom: 1px #e1e4e8 solid !important; - } - - .border-xl-left { - border-left: 1px #e1e4e8 solid !important; - } - - .border-xl-top-0 { - border-top: 0 !important; - } - - .border-xl-right-0 { - border-right: 0 !important; - } - - .border-xl-bottom-0 { - border-bottom: 0 !important; - } - - .border-xl-left-0 { - border-left: 0 !important; - } - - .rounded-xl-0 { - border-radius: 0 !important; - } - - .rounded-xl-1 { - border-radius: 3px !important; - } - - .rounded-xl-2 { - border-radius: 6px !important; - } - - .rounded-xl-top-0 { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; - } - - .rounded-xl-top-1 { - border-top-left-radius: 3px !important; - border-top-right-radius: 3px !important; - } - - .rounded-xl-top-2 { - border-top-left-radius: 6px !important; - border-top-right-radius: 6px !important; - } - - .rounded-xl-right-0 { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - } - - .rounded-xl-right-1 { - border-top-right-radius: 3px !important; - border-bottom-right-radius: 3px !important; - } - - .rounded-xl-right-2 { - border-top-right-radius: 6px !important; - border-bottom-right-radius: 6px !important; - } - - .rounded-xl-bottom-0 { - border-bottom-right-radius: 0 !important; - border-bottom-left-radius: 0 !important; - } - - .rounded-xl-bottom-1 { - border-bottom-right-radius: 3px !important; - border-bottom-left-radius: 3px !important; - } - - .rounded-xl-bottom-2 { - border-bottom-right-radius: 6px !important; - border-bottom-left-radius: 6px !important; - } - - .rounded-xl-left-0 { - border-bottom-left-radius: 0 !important; - border-top-left-radius: 0 !important; - } - - .rounded-xl-left-1 { - border-bottom-left-radius: 3px !important; - border-top-left-radius: 3px !important; - } - - .rounded-xl-left-2 { - border-bottom-left-radius: 6px !important; - border-top-left-radius: 6px !important; - } -} - -.circle { - border-radius: 50% !important; -} - -.box-shadow { - box-shadow: 0 1px 1px rgba(27, 31, 35, 0.1) !important; -} - -.box-shadow-medium { - box-shadow: 0 1px 5px rgba(27, 31, 35, 0.15) !important; -} - -.box-shadow-large { - box-shadow: 0 1px 15px rgba(27, 31, 35, 0.15) !important; -} - -.box-shadow-extra-large { - box-shadow: 0 10px 50px rgba(27, 31, 35, 0.07) !important; -} - -.box-shadow-none { - box-shadow: none !important; -} - -.bg-white { - background-color: #fff !important; -} - -.bg-blue { - background-color: #0366d6 !important; -} - -.bg-blue-light { - background-color: #f1f8ff !important; -} - -.bg-gray-dark { - background-color: #24292e !important; -} - -.bg-gray { - background-color: #f6f8fa !important; -} - -.bg-gray-light { - background-color: #fafbfc !important; -} - -.bg-green { - background-color: #28a745 !important; -} - -.bg-green-light { - background-color: #dcffe4 !important; -} - -.bg-red { - background-color: #d73a49 !important; -} - -.bg-red-light { - background-color: #ffdce0 !important; -} - -.bg-yellow { - background-color: #ffd33d !important; -} - -.bg-yellow-light { - background-color: #fff5b1 !important; -} - -.bg-purple { - background-color: #6f42c1 !important; -} - -.bg-purple-light { - background-color: #f5f0ff !important; -} - -.bg-shade-gradient { - background-image: linear-gradient( - 180deg, - rgba(27, 31, 35, 0.065), - rgba(27, 31, 35, 0) - ) !important; - background-repeat: no-repeat !important; - background-size: 100% 200px !important; -} - -.text-blue { - color: #0366d6 !important; -} - -.text-red { - color: #cb2431 !important; -} - -.text-gray-light { - color: #6a737d !important; -} - -.text-gray { - color: #586069 !important; -} - -.text-gray-dark { - color: #24292e !important; -} - -.text-green { - color: #28a745 !important; -} - -.text-orange { - color: #a04100 !important; -} - -.text-orange-light { - color: #e36209 !important; -} - -.text-purple { - color: #6f42c1 !important; -} - -.text-white { - color: #fff !important; -} - -.text-inherit { - color: inherit !important; -} - -.text-pending { - color: #b08800 !important; -} - -.bg-pending { - color: #dbab09 !important; -} - -.link-gray { - color: #586069 !important; -} - -.link-gray:hover { - color: #0366d6 !important; -} - -.link-gray-dark { - color: #24292e !important; -} - -.link-gray-dark:hover { - color: #0366d6 !important; -} - -.link-hover-blue:hover { - color: #0366d6 !important; -} - -.muted-link { - color: #586069 !important; -} - -.muted-link:hover { - color: #0366d6 !important; - text-decoration: none; -} - -.details-overlay[open] > summary::before { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 80; - display: block; - cursor: default; - content: " "; - background: transparent; -} - -.details-overlay-dark[open] > summary::before { - z-index: 99; - background: rgba(27, 31, 35, 0.5); -} - -.flex-row { - flex-direction: row !important; -} - -.flex-row-reverse { - flex-direction: row-reverse !important; -} - -.flex-column { - flex-direction: column !important; -} - -.flex-wrap { - flex-wrap: wrap !important; -} - -.flex-nowrap { - flex-wrap: nowrap !important; -} - -.flex-justify-start { - justify-content: flex-start !important; -} - -.flex-justify-end { - justify-content: flex-end !important; -} - -.flex-justify-center { - justify-content: center !important; -} - -.flex-justify-between { - justify-content: space-between !important; -} - -.flex-justify-around { - justify-content: space-around !important; -} - -.flex-items-start { - align-items: flex-start !important; -} - -.flex-items-end { - align-items: flex-end !important; -} - -.flex-items-center { - align-items: center !important; -} - -.flex-items-baseline { - align-items: baseline !important; -} - -.flex-items-stretch { - align-items: stretch !important; -} - -.flex-content-start { - align-content: flex-start !important; -} - -.flex-content-end { - align-content: flex-end !important; -} - -.flex-content-center { - align-content: center !important; -} - -.flex-content-between { - align-content: space-between !important; -} - -.flex-content-around { - align-content: space-around !important; -} - -.flex-content-stretch { - align-content: stretch !important; -} - -.flex-auto { - flex: 1 1 auto !important; -} - -.flex-shrink-0 { - flex-shrink: 0 !important; -} - -.flex-self-auto { - align-self: auto !important; -} - -.flex-self-start { - align-self: flex-start !important; -} - -.flex-self-end { - align-self: flex-end !important; -} - -.flex-self-center { - align-self: center !important; -} - -.flex-self-baseline { - align-self: baseline !important; -} - -.flex-self-stretch { - align-self: stretch !important; -} - -.flex-item-equal { - flex-grow: 1; - flex-basis: 0; -} - -@media (min-width: 544px) { - .flex-sm-row { - flex-direction: row !important; - } - - .flex-sm-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-sm-column { - flex-direction: column !important; - } - - .flex-sm-wrap { - flex-wrap: wrap !important; - } - - .flex-sm-nowrap { - flex-wrap: nowrap !important; - } - - .flex-sm-justify-start { - justify-content: flex-start !important; - } - - .flex-sm-justify-end { - justify-content: flex-end !important; - } - - .flex-sm-justify-center { - justify-content: center !important; - } - - .flex-sm-justify-between { - justify-content: space-between !important; - } - - .flex-sm-justify-around { - justify-content: space-around !important; - } - - .flex-sm-items-start { - align-items: flex-start !important; - } - - .flex-sm-items-end { - align-items: flex-end !important; - } - - .flex-sm-items-center { - align-items: center !important; - } - - .flex-sm-items-baseline { - align-items: baseline !important; - } - - .flex-sm-items-stretch { - align-items: stretch !important; - } - - .flex-sm-content-start { - align-content: flex-start !important; - } - - .flex-sm-content-end { - align-content: flex-end !important; - } - - .flex-sm-content-center { - align-content: center !important; - } - - .flex-sm-content-between { - align-content: space-between !important; - } - - .flex-sm-content-around { - align-content: space-around !important; - } - - .flex-sm-content-stretch { - align-content: stretch !important; - } - - .flex-sm-auto { - flex: 1 1 auto !important; - } - - .flex-sm-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-sm-self-auto { - align-self: auto !important; - } - - .flex-sm-self-start { - align-self: flex-start !important; - } - - .flex-sm-self-end { - align-self: flex-end !important; - } - - .flex-sm-self-center { - align-self: center !important; - } - - .flex-sm-self-baseline { - align-self: baseline !important; - } - - .flex-sm-self-stretch { - align-self: stretch !important; - } - - .flex-sm-item-equal { - flex-grow: 1; - flex-basis: 0; - } -} - -@media (min-width: 768px) { - .flex-md-row { - flex-direction: row !important; - } - - .flex-md-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-md-column { - flex-direction: column !important; - } - - .flex-md-wrap { - flex-wrap: wrap !important; - } - - .flex-md-nowrap { - flex-wrap: nowrap !important; - } - - .flex-md-justify-start { - justify-content: flex-start !important; - } - - .flex-md-justify-end { - justify-content: flex-end !important; - } - - .flex-md-justify-center { - justify-content: center !important; - } - - .flex-md-justify-between { - justify-content: space-between !important; - } - - .flex-md-justify-around { - justify-content: space-around !important; - } - - .flex-md-items-start { - align-items: flex-start !important; - } - - .flex-md-items-end { - align-items: flex-end !important; - } - - .flex-md-items-center { - align-items: center !important; - } - - .flex-md-items-baseline { - align-items: baseline !important; - } - - .flex-md-items-stretch { - align-items: stretch !important; - } - - .flex-md-content-start { - align-content: flex-start !important; - } - - .flex-md-content-end { - align-content: flex-end !important; - } - - .flex-md-content-center { - align-content: center !important; - } - - .flex-md-content-between { - align-content: space-between !important; - } - - .flex-md-content-around { - align-content: space-around !important; - } - - .flex-md-content-stretch { - align-content: stretch !important; - } - - .flex-md-auto { - flex: 1 1 auto !important; - } - - .flex-md-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-md-self-auto { - align-self: auto !important; - } - - .flex-md-self-start { - align-self: flex-start !important; - } - - .flex-md-self-end { - align-self: flex-end !important; - } - - .flex-md-self-center { - align-self: center !important; - } - - .flex-md-self-baseline { - align-self: baseline !important; - } - - .flex-md-self-stretch { - align-self: stretch !important; - } - - .flex-md-item-equal { - flex-grow: 1; - flex-basis: 0; - } -} - -@media (min-width: 1012px) { - .flex-lg-row { - flex-direction: row !important; - } - - .flex-lg-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-lg-column { - flex-direction: column !important; - } - - .flex-lg-wrap { - flex-wrap: wrap !important; - } - - .flex-lg-nowrap { - flex-wrap: nowrap !important; - } - - .flex-lg-justify-start { - justify-content: flex-start !important; - } - - .flex-lg-justify-end { - justify-content: flex-end !important; - } - - .flex-lg-justify-center { - justify-content: center !important; - } - - .flex-lg-justify-between { - justify-content: space-between !important; - } - - .flex-lg-justify-around { - justify-content: space-around !important; - } - - .flex-lg-items-start { - align-items: flex-start !important; - } - - .flex-lg-items-end { - align-items: flex-end !important; - } - - .flex-lg-items-center { - align-items: center !important; - } - - .flex-lg-items-baseline { - align-items: baseline !important; - } - - .flex-lg-items-stretch { - align-items: stretch !important; - } - - .flex-lg-content-start { - align-content: flex-start !important; - } - - .flex-lg-content-end { - align-content: flex-end !important; - } - - .flex-lg-content-center { - align-content: center !important; - } - - .flex-lg-content-between { - align-content: space-between !important; - } - - .flex-lg-content-around { - align-content: space-around !important; - } - - .flex-lg-content-stretch { - align-content: stretch !important; - } - - .flex-lg-auto { - flex: 1 1 auto !important; - } - - .flex-lg-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-lg-self-auto { - align-self: auto !important; - } - - .flex-lg-self-start { - align-self: flex-start !important; - } - - .flex-lg-self-end { - align-self: flex-end !important; - } - - .flex-lg-self-center { - align-self: center !important; - } - - .flex-lg-self-baseline { - align-self: baseline !important; - } - - .flex-lg-self-stretch { - align-self: stretch !important; - } - - .flex-lg-item-equal { - flex-grow: 1; - flex-basis: 0; - } -} - -@media (min-width: 1280px) { - .flex-xl-row { - flex-direction: row !important; - } - - .flex-xl-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-xl-column { - flex-direction: column !important; - } - - .flex-xl-wrap { - flex-wrap: wrap !important; - } - - .flex-xl-nowrap { - flex-wrap: nowrap !important; - } - - .flex-xl-justify-start { - justify-content: flex-start !important; - } - - .flex-xl-justify-end { - justify-content: flex-end !important; - } - - .flex-xl-justify-center { - justify-content: center !important; - } - - .flex-xl-justify-between { - justify-content: space-between !important; - } - - .flex-xl-justify-around { - justify-content: space-around !important; - } - - .flex-xl-items-start { - align-items: flex-start !important; - } - - .flex-xl-items-end { - align-items: flex-end !important; - } - - .flex-xl-items-center { - align-items: center !important; - } - - .flex-xl-items-baseline { - align-items: baseline !important; - } - - .flex-xl-items-stretch { - align-items: stretch !important; - } - - .flex-xl-content-start { - align-content: flex-start !important; - } - - .flex-xl-content-end { - align-content: flex-end !important; - } - - .flex-xl-content-center { - align-content: center !important; - } - - .flex-xl-content-between { - align-content: space-between !important; - } - - .flex-xl-content-around { - align-content: space-around !important; - } - - .flex-xl-content-stretch { - align-content: stretch !important; - } - - .flex-xl-auto { - flex: 1 1 auto !important; - } - - .flex-xl-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-xl-self-auto { - align-self: auto !important; - } - - .flex-xl-self-start { - align-self: flex-start !important; - } - - .flex-xl-self-end { - align-self: flex-end !important; - } - - .flex-xl-self-center { - align-self: center !important; - } - - .flex-xl-self-baseline { - align-self: baseline !important; - } - - .flex-xl-self-stretch { - align-self: stretch !important; - } - - .flex-xl-item-equal { - flex-grow: 1; - flex-basis: 0; - } -} - -.position-static { - position: static !important; -} - -.position-relative { - position: relative !important; -} - -.position-absolute { - position: absolute !important; -} - -.position-fixed { - position: fixed !important; -} - -.top-0 { - top: 0 !important; -} - -.right-0 { - right: 0 !important; -} - -.bottom-0 { - bottom: 0 !important; -} - -.left-0 { - left: 0 !important; -} - -.v-align-middle { - vertical-align: middle !important; -} - -.v-align-top { - vertical-align: top !important; -} - -.v-align-bottom { - vertical-align: bottom !important; -} - -.v-align-text-top { - vertical-align: text-top !important; -} - -.v-align-text-bottom { - vertical-align: text-bottom !important; -} - -.v-align-baseline { - vertical-align: baseline !important; -} - -.overflow-hidden { - overflow: hidden !important; -} - -.overflow-scroll { - overflow: scroll !important; -} - -.overflow-auto { - overflow: auto !important; -} - -.clearfix::before { - display: table; - content: ""; -} - -.clearfix::after { - display: table; - clear: both; - content: ""; -} - -.float-left { - float: left !important; -} - -.float-right { - float: right !important; -} - -.float-none { - float: none !important; -} - -@media (min-width: 544px) { - .float-sm-left { - float: left !important; - } - - .float-sm-right { - float: right !important; - } - - .float-sm-none { - float: none !important; - } -} - -@media (min-width: 768px) { - .float-md-left { - float: left !important; - } - - .float-md-right { - float: right !important; - } - - .float-md-none { - float: none !important; - } -} - -@media (min-width: 1012px) { - .float-lg-left { - float: left !important; - } - - .float-lg-right { - float: right !important; - } - - .float-lg-none { - float: none !important; - } -} - -@media (min-width: 1280px) { - .float-xl-left { - float: left !important; - } - - .float-xl-right { - float: right !important; - } - - .float-xl-none { - float: none !important; - } -} - -.width-fit { - max-width: 100% !important; -} - -.width-full { - width: 100% !important; -} - -.height-fit { - max-height: 100% !important; -} - -.height-full { - height: 100% !important; -} - -.min-width-0 { - min-width: 0 !important; -} - -.direction-rtl { - direction: rtl !important; -} - -.direction-ltr { - direction: ltr !important; -} - -@media (min-width: 544px) { - .direction-sm-rtl { - direction: rtl !important; - } - - .direction-sm-ltr { - direction: ltr !important; - } -} - -@media (min-width: 768px) { - .direction-md-rtl { - direction: rtl !important; - } - - .direction-md-ltr { - direction: ltr !important; - } -} - -@media (min-width: 1012px) { - .direction-lg-rtl { - direction: rtl !important; - } - - .direction-lg-ltr { - direction: ltr !important; - } -} - -@media (min-width: 1280px) { - .direction-xl-rtl { - direction: rtl !important; - } - - .direction-xl-ltr { - direction: ltr !important; - } -} - -.m-0 { - margin: 0 !important; -} - -.mt-0 { - margin-top: 0 !important; -} - -.mr-0 { - margin-right: 0 !important; -} - -.mb-0 { - margin-bottom: 0 !important; -} - -.ml-0 { - margin-left: 0 !important; -} - -.mx-0 { - margin-right: 0 !important; - margin-left: 0 !important; -} - -.my-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.m-1 { - margin: 4px !important; -} - -.mt-1 { - margin-top: 4px !important; -} - -.mr-1 { - margin-right: 4px !important; -} - -.mb-1 { - margin-bottom: 4px !important; -} - -.ml-1 { - margin-left: 4px !important; -} - -.mt-n1 { - margin-top: -4px !important; -} - -.mr-n1 { - margin-right: -4px !important; -} - -.mb-n1 { - margin-bottom: -4px !important; -} - -.ml-n1 { - margin-left: -4px !important; -} - -.mx-1 { - margin-right: 4px !important; - margin-left: 4px !important; -} - -.my-1 { - margin-top: 4px !important; - margin-bottom: 4px !important; -} - -.m-2 { - margin: 8px !important; -} - -.mt-2 { - margin-top: 8px !important; -} - -.mr-2 { - margin-right: 8px !important; -} - -.mb-2 { - margin-bottom: 8px !important; -} - -.ml-2 { - margin-left: 8px !important; -} - -.mt-n2 { - margin-top: -8px !important; -} - -.mr-n2 { - margin-right: -8px !important; -} - -.mb-n2 { - margin-bottom: -8px !important; -} - -.ml-n2 { - margin-left: -8px !important; -} - -.mx-2 { - margin-right: 8px !important; - margin-left: 8px !important; -} - -.my-2 { - margin-top: 8px !important; - margin-bottom: 8px !important; -} - -.m-3 { - margin: 16px !important; -} - -.mt-3 { - margin-top: 16px !important; -} - -.mr-3 { - margin-right: 16px !important; -} - -.mb-3 { - margin-bottom: 16px !important; -} - -.ml-3 { - margin-left: 16px !important; -} - -.mt-n3 { - margin-top: -16px !important; -} - -.mr-n3 { - margin-right: -16px !important; -} - -.mb-n3 { - margin-bottom: -16px !important; -} - -.ml-n3 { - margin-left: -16px !important; -} - -.mx-3 { - margin-right: 16px !important; - margin-left: 16px !important; -} - -.my-3 { - margin-top: 16px !important; - margin-bottom: 16px !important; -} - -.m-4 { - margin: 24px !important; -} - -.mt-4 { - margin-top: 24px !important; -} - -.mr-4 { - margin-right: 24px !important; -} - -.mb-4 { - margin-bottom: 24px !important; -} - -.ml-4 { - margin-left: 24px !important; -} - -.mt-n4 { - margin-top: -24px !important; -} - -.mr-n4 { - margin-right: -24px !important; -} - -.mb-n4 { - margin-bottom: -24px !important; -} - -.ml-n4 { - margin-left: -24px !important; -} - -.mx-4 { - margin-right: 24px !important; - margin-left: 24px !important; -} - -.my-4 { - margin-top: 24px !important; - margin-bottom: 24px !important; -} - -.m-5 { - margin: 32px !important; -} - -.mt-5 { - margin-top: 32px !important; -} - -.mr-5 { - margin-right: 32px !important; -} - -.mb-5 { - margin-bottom: 32px !important; -} - -.ml-5 { - margin-left: 32px !important; -} - -.mt-n5 { - margin-top: -32px !important; -} - -.mr-n5 { - margin-right: -32px !important; -} - -.mb-n5 { - margin-bottom: -32px !important; -} - -.ml-n5 { - margin-left: -32px !important; -} - -.mx-5 { - margin-right: 32px !important; - margin-left: 32px !important; -} - -.my-5 { - margin-top: 32px !important; - margin-bottom: 32px !important; -} - -.m-6 { - margin: 40px !important; -} - -.mt-6 { - margin-top: 40px !important; -} - -.mr-6 { - margin-right: 40px !important; -} - -.mb-6 { - margin-bottom: 40px !important; -} - -.ml-6 { - margin-left: 40px !important; -} - -.mt-n6 { - margin-top: -40px !important; -} - -.mr-n6 { - margin-right: -40px !important; -} - -.mb-n6 { - margin-bottom: -40px !important; -} - -.ml-n6 { - margin-left: -40px !important; -} - -.mx-6 { - margin-right: 40px !important; - margin-left: 40px !important; -} - -.my-6 { - margin-top: 40px !important; - margin-bottom: 40px !important; -} - -.mx-auto { - margin-right: auto !important; - margin-left: auto !important; -} - -@media (min-width: 544px) { - .m-sm-0 { - margin: 0 !important; - } - - .mt-sm-0 { - margin-top: 0 !important; - } - - .mr-sm-0 { - margin-right: 0 !important; - } - - .mb-sm-0 { - margin-bottom: 0 !important; - } - - .ml-sm-0 { - margin-left: 0 !important; - } - - .mx-sm-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .my-sm-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .m-sm-1 { - margin: 4px !important; - } - - .mt-sm-1 { - margin-top: 4px !important; - } - - .mr-sm-1 { - margin-right: 4px !important; - } - - .mb-sm-1 { - margin-bottom: 4px !important; - } - - .ml-sm-1 { - margin-left: 4px !important; - } - - .mt-sm-n1 { - margin-top: -4px !important; - } - - .mr-sm-n1 { - margin-right: -4px !important; - } - - .mb-sm-n1 { - margin-bottom: -4px !important; - } - - .ml-sm-n1 { - margin-left: -4px !important; - } - - .mx-sm-1 { - margin-right: 4px !important; - margin-left: 4px !important; - } - - .my-sm-1 { - margin-top: 4px !important; - margin-bottom: 4px !important; - } - - .m-sm-2 { - margin: 8px !important; - } - - .mt-sm-2 { - margin-top: 8px !important; - } - - .mr-sm-2 { - margin-right: 8px !important; - } - - .mb-sm-2 { - margin-bottom: 8px !important; - } - - .ml-sm-2 { - margin-left: 8px !important; - } - - .mt-sm-n2 { - margin-top: -8px !important; - } - - .mr-sm-n2 { - margin-right: -8px !important; - } - - .mb-sm-n2 { - margin-bottom: -8px !important; - } - - .ml-sm-n2 { - margin-left: -8px !important; - } - - .mx-sm-2 { - margin-right: 8px !important; - margin-left: 8px !important; - } - - .my-sm-2 { - margin-top: 8px !important; - margin-bottom: 8px !important; - } - - .m-sm-3 { - margin: 16px !important; - } - - .mt-sm-3 { - margin-top: 16px !important; - } - - .mr-sm-3 { - margin-right: 16px !important; - } - - .mb-sm-3 { - margin-bottom: 16px !important; - } - - .ml-sm-3 { - margin-left: 16px !important; - } - - .mt-sm-n3 { - margin-top: -16px !important; - } - - .mr-sm-n3 { - margin-right: -16px !important; - } - - .mb-sm-n3 { - margin-bottom: -16px !important; - } - - .ml-sm-n3 { - margin-left: -16px !important; - } - - .mx-sm-3 { - margin-right: 16px !important; - margin-left: 16px !important; - } - - .my-sm-3 { - margin-top: 16px !important; - margin-bottom: 16px !important; - } - - .m-sm-4 { - margin: 24px !important; - } - - .mt-sm-4 { - margin-top: 24px !important; - } - - .mr-sm-4 { - margin-right: 24px !important; - } - - .mb-sm-4 { - margin-bottom: 24px !important; - } - - .ml-sm-4 { - margin-left: 24px !important; - } - - .mt-sm-n4 { - margin-top: -24px !important; - } - - .mr-sm-n4 { - margin-right: -24px !important; - } - - .mb-sm-n4 { - margin-bottom: -24px !important; - } - - .ml-sm-n4 { - margin-left: -24px !important; - } - - .mx-sm-4 { - margin-right: 24px !important; - margin-left: 24px !important; - } - - .my-sm-4 { - margin-top: 24px !important; - margin-bottom: 24px !important; - } - - .m-sm-5 { - margin: 32px !important; - } - - .mt-sm-5 { - margin-top: 32px !important; - } - - .mr-sm-5 { - margin-right: 32px !important; - } - - .mb-sm-5 { - margin-bottom: 32px !important; - } - - .ml-sm-5 { - margin-left: 32px !important; - } - - .mt-sm-n5 { - margin-top: -32px !important; - } - - .mr-sm-n5 { - margin-right: -32px !important; - } - - .mb-sm-n5 { - margin-bottom: -32px !important; - } - - .ml-sm-n5 { - margin-left: -32px !important; - } - - .mx-sm-5 { - margin-right: 32px !important; - margin-left: 32px !important; - } - - .my-sm-5 { - margin-top: 32px !important; - margin-bottom: 32px !important; - } - - .m-sm-6 { - margin: 40px !important; - } - - .mt-sm-6 { - margin-top: 40px !important; - } - - .mr-sm-6 { - margin-right: 40px !important; - } - - .mb-sm-6 { - margin-bottom: 40px !important; - } - - .ml-sm-6 { - margin-left: 40px !important; - } - - .mt-sm-n6 { - margin-top: -40px !important; - } - - .mr-sm-n6 { - margin-right: -40px !important; - } - - .mb-sm-n6 { - margin-bottom: -40px !important; - } - - .ml-sm-n6 { - margin-left: -40px !important; - } - - .mx-sm-6 { - margin-right: 40px !important; - margin-left: 40px !important; - } - - .my-sm-6 { - margin-top: 40px !important; - margin-bottom: 40px !important; - } - - .mx-sm-auto { - margin-right: auto !important; - margin-left: auto !important; - } -} - -@media (min-width: 768px) { - .m-md-0 { - margin: 0 !important; - } - - .mt-md-0 { - margin-top: 0 !important; - } - - .mr-md-0 { - margin-right: 0 !important; - } - - .mb-md-0 { - margin-bottom: 0 !important; - } - - .ml-md-0 { - margin-left: 0 !important; - } - - .mx-md-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .my-md-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .m-md-1 { - margin: 4px !important; - } - - .mt-md-1 { - margin-top: 4px !important; - } - - .mr-md-1 { - margin-right: 4px !important; - } - - .mb-md-1 { - margin-bottom: 4px !important; - } - - .ml-md-1 { - margin-left: 4px !important; - } - - .mt-md-n1 { - margin-top: -4px !important; - } - - .mr-md-n1 { - margin-right: -4px !important; - } - - .mb-md-n1 { - margin-bottom: -4px !important; - } - - .ml-md-n1 { - margin-left: -4px !important; - } - - .mx-md-1 { - margin-right: 4px !important; - margin-left: 4px !important; - } - - .my-md-1 { - margin-top: 4px !important; - margin-bottom: 4px !important; - } - - .m-md-2 { - margin: 8px !important; - } - - .mt-md-2 { - margin-top: 8px !important; - } - - .mr-md-2 { - margin-right: 8px !important; - } - - .mb-md-2 { - margin-bottom: 8px !important; - } - - .ml-md-2 { - margin-left: 8px !important; - } - - .mt-md-n2 { - margin-top: -8px !important; - } - - .mr-md-n2 { - margin-right: -8px !important; - } - - .mb-md-n2 { - margin-bottom: -8px !important; - } - - .ml-md-n2 { - margin-left: -8px !important; - } - - .mx-md-2 { - margin-right: 8px !important; - margin-left: 8px !important; - } - - .my-md-2 { - margin-top: 8px !important; - margin-bottom: 8px !important; - } - - .m-md-3 { - margin: 16px !important; - } - - .mt-md-3 { - margin-top: 16px !important; - } - - .mr-md-3 { - margin-right: 16px !important; - } - - .mb-md-3 { - margin-bottom: 16px !important; - } - - .ml-md-3 { - margin-left: 16px !important; - } - - .mt-md-n3 { - margin-top: -16px !important; - } - - .mr-md-n3 { - margin-right: -16px !important; - } - - .mb-md-n3 { - margin-bottom: -16px !important; - } - - .ml-md-n3 { - margin-left: -16px !important; - } - - .mx-md-3 { - margin-right: 16px !important; - margin-left: 16px !important; - } - - .my-md-3 { - margin-top: 16px !important; - margin-bottom: 16px !important; - } - - .m-md-4 { - margin: 24px !important; - } - - .mt-md-4 { - margin-top: 24px !important; - } - - .mr-md-4 { - margin-right: 24px !important; - } - - .mb-md-4 { - margin-bottom: 24px !important; - } - - .ml-md-4 { - margin-left: 24px !important; - } - - .mt-md-n4 { - margin-top: -24px !important; - } - - .mr-md-n4 { - margin-right: -24px !important; - } - - .mb-md-n4 { - margin-bottom: -24px !important; - } - - .ml-md-n4 { - margin-left: -24px !important; - } - - .mx-md-4 { - margin-right: 24px !important; - margin-left: 24px !important; - } - - .my-md-4 { - margin-top: 24px !important; - margin-bottom: 24px !important; - } - - .m-md-5 { - margin: 32px !important; - } - - .mt-md-5 { - margin-top: 32px !important; - } - - .mr-md-5 { - margin-right: 32px !important; - } - - .mb-md-5 { - margin-bottom: 32px !important; - } - - .ml-md-5 { - margin-left: 32px !important; - } - - .mt-md-n5 { - margin-top: -32px !important; - } - - .mr-md-n5 { - margin-right: -32px !important; - } - - .mb-md-n5 { - margin-bottom: -32px !important; - } - - .ml-md-n5 { - margin-left: -32px !important; - } - - .mx-md-5 { - margin-right: 32px !important; - margin-left: 32px !important; - } - - .my-md-5 { - margin-top: 32px !important; - margin-bottom: 32px !important; - } - - .m-md-6 { - margin: 40px !important; - } - - .mt-md-6 { - margin-top: 40px !important; - } - - .mr-md-6 { - margin-right: 40px !important; - } - - .mb-md-6 { - margin-bottom: 40px !important; - } - - .ml-md-6 { - margin-left: 40px !important; - } - - .mt-md-n6 { - margin-top: -40px !important; - } - - .mr-md-n6 { - margin-right: -40px !important; - } - - .mb-md-n6 { - margin-bottom: -40px !important; - } - - .ml-md-n6 { - margin-left: -40px !important; - } - - .mx-md-6 { - margin-right: 40px !important; - margin-left: 40px !important; - } - - .my-md-6 { - margin-top: 40px !important; - margin-bottom: 40px !important; - } - - .mx-md-auto { - margin-right: auto !important; - margin-left: auto !important; - } -} - -@media (min-width: 1012px) { - .m-lg-0 { - margin: 0 !important; - } - - .mt-lg-0 { - margin-top: 0 !important; - } - - .mr-lg-0 { - margin-right: 0 !important; - } - - .mb-lg-0 { - margin-bottom: 0 !important; - } - - .ml-lg-0 { - margin-left: 0 !important; - } - - .mx-lg-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .my-lg-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .m-lg-1 { - margin: 4px !important; - } - - .mt-lg-1 { - margin-top: 4px !important; - } - - .mr-lg-1 { - margin-right: 4px !important; - } - - .mb-lg-1 { - margin-bottom: 4px !important; - } - - .ml-lg-1 { - margin-left: 4px !important; - } - - .mt-lg-n1 { - margin-top: -4px !important; - } - - .mr-lg-n1 { - margin-right: -4px !important; - } - - .mb-lg-n1 { - margin-bottom: -4px !important; - } - - .ml-lg-n1 { - margin-left: -4px !important; - } - - .mx-lg-1 { - margin-right: 4px !important; - margin-left: 4px !important; - } - - .my-lg-1 { - margin-top: 4px !important; - margin-bottom: 4px !important; - } - - .m-lg-2 { - margin: 8px !important; - } - - .mt-lg-2 { - margin-top: 8px !important; - } - - .mr-lg-2 { - margin-right: 8px !important; - } - - .mb-lg-2 { - margin-bottom: 8px !important; - } - - .ml-lg-2 { - margin-left: 8px !important; - } - - .mt-lg-n2 { - margin-top: -8px !important; - } - - .mr-lg-n2 { - margin-right: -8px !important; - } - - .mb-lg-n2 { - margin-bottom: -8px !important; - } - - .ml-lg-n2 { - margin-left: -8px !important; - } - - .mx-lg-2 { - margin-right: 8px !important; - margin-left: 8px !important; - } - - .my-lg-2 { - margin-top: 8px !important; - margin-bottom: 8px !important; - } - - .m-lg-3 { - margin: 16px !important; - } - - .mt-lg-3 { - margin-top: 16px !important; - } - - .mr-lg-3 { - margin-right: 16px !important; - } - - .mb-lg-3 { - margin-bottom: 16px !important; - } - - .ml-lg-3 { - margin-left: 16px !important; - } - - .mt-lg-n3 { - margin-top: -16px !important; - } - - .mr-lg-n3 { - margin-right: -16px !important; - } - - .mb-lg-n3 { - margin-bottom: -16px !important; - } - - .ml-lg-n3 { - margin-left: -16px !important; - } - - .mx-lg-3 { - margin-right: 16px !important; - margin-left: 16px !important; - } - - .my-lg-3 { - margin-top: 16px !important; - margin-bottom: 16px !important; - } - - .m-lg-4 { - margin: 24px !important; - } - - .mt-lg-4 { - margin-top: 24px !important; - } - - .mr-lg-4 { - margin-right: 24px !important; - } - - .mb-lg-4 { - margin-bottom: 24px !important; - } - - .ml-lg-4 { - margin-left: 24px !important; - } - - .mt-lg-n4 { - margin-top: -24px !important; - } - - .mr-lg-n4 { - margin-right: -24px !important; - } - - .mb-lg-n4 { - margin-bottom: -24px !important; - } - - .ml-lg-n4 { - margin-left: -24px !important; - } - - .mx-lg-4 { - margin-right: 24px !important; - margin-left: 24px !important; - } - - .my-lg-4 { - margin-top: 24px !important; - margin-bottom: 24px !important; - } - - .m-lg-5 { - margin: 32px !important; - } - - .mt-lg-5 { - margin-top: 32px !important; - } - - .mr-lg-5 { - margin-right: 32px !important; - } - - .mb-lg-5 { - margin-bottom: 32px !important; - } - - .ml-lg-5 { - margin-left: 32px !important; - } - - .mt-lg-n5 { - margin-top: -32px !important; - } - - .mr-lg-n5 { - margin-right: -32px !important; - } - - .mb-lg-n5 { - margin-bottom: -32px !important; - } - - .ml-lg-n5 { - margin-left: -32px !important; - } - - .mx-lg-5 { - margin-right: 32px !important; - margin-left: 32px !important; - } - - .my-lg-5 { - margin-top: 32px !important; - margin-bottom: 32px !important; - } - - .m-lg-6 { - margin: 40px !important; - } - - .mt-lg-6 { - margin-top: 40px !important; - } - - .mr-lg-6 { - margin-right: 40px !important; - } - - .mb-lg-6 { - margin-bottom: 40px !important; - } - - .ml-lg-6 { - margin-left: 40px !important; - } - - .mt-lg-n6 { - margin-top: -40px !important; - } - - .mr-lg-n6 { - margin-right: -40px !important; - } - - .mb-lg-n6 { - margin-bottom: -40px !important; - } - - .ml-lg-n6 { - margin-left: -40px !important; - } - - .mx-lg-6 { - margin-right: 40px !important; - margin-left: 40px !important; - } - - .my-lg-6 { - margin-top: 40px !important; - margin-bottom: 40px !important; - } - - .mx-lg-auto { - margin-right: auto !important; - margin-left: auto !important; - } -} - -@media (min-width: 1280px) { - .m-xl-0 { - margin: 0 !important; - } - - .mt-xl-0 { - margin-top: 0 !important; - } - - .mr-xl-0 { - margin-right: 0 !important; - } - - .mb-xl-0 { - margin-bottom: 0 !important; - } - - .ml-xl-0 { - margin-left: 0 !important; - } - - .mx-xl-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .my-xl-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .m-xl-1 { - margin: 4px !important; - } - - .mt-xl-1 { - margin-top: 4px !important; - } - - .mr-xl-1 { - margin-right: 4px !important; - } - - .mb-xl-1 { - margin-bottom: 4px !important; - } - - .ml-xl-1 { - margin-left: 4px !important; - } - - .mt-xl-n1 { - margin-top: -4px !important; - } - - .mr-xl-n1 { - margin-right: -4px !important; - } - - .mb-xl-n1 { - margin-bottom: -4px !important; - } - - .ml-xl-n1 { - margin-left: -4px !important; - } - - .mx-xl-1 { - margin-right: 4px !important; - margin-left: 4px !important; - } - - .my-xl-1 { - margin-top: 4px !important; - margin-bottom: 4px !important; - } - - .m-xl-2 { - margin: 8px !important; - } - - .mt-xl-2 { - margin-top: 8px !important; - } - - .mr-xl-2 { - margin-right: 8px !important; - } - - .mb-xl-2 { - margin-bottom: 8px !important; - } - - .ml-xl-2 { - margin-left: 8px !important; - } - - .mt-xl-n2 { - margin-top: -8px !important; - } - - .mr-xl-n2 { - margin-right: -8px !important; - } - - .mb-xl-n2 { - margin-bottom: -8px !important; - } - - .ml-xl-n2 { - margin-left: -8px !important; - } - - .mx-xl-2 { - margin-right: 8px !important; - margin-left: 8px !important; - } - - .my-xl-2 { - margin-top: 8px !important; - margin-bottom: 8px !important; - } - - .m-xl-3 { - margin: 16px !important; - } - - .mt-xl-3 { - margin-top: 16px !important; - } - - .mr-xl-3 { - margin-right: 16px !important; - } - - .mb-xl-3 { - margin-bottom: 16px !important; - } - - .ml-xl-3 { - margin-left: 16px !important; - } - - .mt-xl-n3 { - margin-top: -16px !important; - } - - .mr-xl-n3 { - margin-right: -16px !important; - } - - .mb-xl-n3 { - margin-bottom: -16px !important; - } - - .ml-xl-n3 { - margin-left: -16px !important; - } - - .mx-xl-3 { - margin-right: 16px !important; - margin-left: 16px !important; - } - - .my-xl-3 { - margin-top: 16px !important; - margin-bottom: 16px !important; - } - - .m-xl-4 { - margin: 24px !important; - } - - .mt-xl-4 { - margin-top: 24px !important; - } - - .mr-xl-4 { - margin-right: 24px !important; - } - - .mb-xl-4 { - margin-bottom: 24px !important; - } - - .ml-xl-4 { - margin-left: 24px !important; - } - - .mt-xl-n4 { - margin-top: -24px !important; - } - - .mr-xl-n4 { - margin-right: -24px !important; - } - - .mb-xl-n4 { - margin-bottom: -24px !important; - } - - .ml-xl-n4 { - margin-left: -24px !important; - } - - .mx-xl-4 { - margin-right: 24px !important; - margin-left: 24px !important; - } - - .my-xl-4 { - margin-top: 24px !important; - margin-bottom: 24px !important; - } - - .m-xl-5 { - margin: 32px !important; - } - - .mt-xl-5 { - margin-top: 32px !important; - } - - .mr-xl-5 { - margin-right: 32px !important; - } - - .mb-xl-5 { - margin-bottom: 32px !important; - } - - .ml-xl-5 { - margin-left: 32px !important; - } - - .mt-xl-n5 { - margin-top: -32px !important; - } - - .mr-xl-n5 { - margin-right: -32px !important; - } - - .mb-xl-n5 { - margin-bottom: -32px !important; - } - - .ml-xl-n5 { - margin-left: -32px !important; - } - - .mx-xl-5 { - margin-right: 32px !important; - margin-left: 32px !important; - } - - .my-xl-5 { - margin-top: 32px !important; - margin-bottom: 32px !important; - } - - .m-xl-6 { - margin: 40px !important; - } - - .mt-xl-6 { - margin-top: 40px !important; - } - - .mr-xl-6 { - margin-right: 40px !important; - } - - .mb-xl-6 { - margin-bottom: 40px !important; - } - - .ml-xl-6 { - margin-left: 40px !important; - } - - .mt-xl-n6 { - margin-top: -40px !important; - } - - .mr-xl-n6 { - margin-right: -40px !important; - } - - .mb-xl-n6 { - margin-bottom: -40px !important; - } - - .ml-xl-n6 { - margin-left: -40px !important; - } - - .mx-xl-6 { - margin-right: 40px !important; - margin-left: 40px !important; - } - - .my-xl-6 { - margin-top: 40px !important; - margin-bottom: 40px !important; - } - - .mx-xl-auto { - margin-right: auto !important; - margin-left: auto !important; - } -} - -.p-0 { - padding: 0 !important; -} - -.pt-0 { - padding-top: 0 !important; -} - -.pr-0 { - padding-right: 0 !important; -} - -.pb-0 { - padding-bottom: 0 !important; -} - -.pl-0 { - padding-left: 0 !important; -} - -.px-0 { - padding-right: 0 !important; - padding-left: 0 !important; -} - -.py-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; -} - -.p-1 { - padding: 4px !important; -} - -.pt-1 { - padding-top: 4px !important; -} - -.pr-1 { - padding-right: 4px !important; -} - -.pb-1 { - padding-bottom: 4px !important; -} - -.pl-1 { - padding-left: 4px !important; -} - -.px-1 { - padding-right: 4px !important; - padding-left: 4px !important; -} - -.py-1 { - padding-top: 4px !important; - padding-bottom: 4px !important; -} - -.p-2 { - padding: 8px !important; -} - -.pt-2 { - padding-top: 8px !important; -} - -.pr-2 { - padding-right: 8px !important; -} - -.pb-2 { - padding-bottom: 8px !important; -} - -.pl-2 { - padding-left: 8px !important; -} - -.px-2 { - padding-right: 8px !important; - padding-left: 8px !important; -} - -.py-2 { - padding-top: 8px !important; - padding-bottom: 8px !important; -} - -.p-3 { - padding: 16px !important; -} - -.pt-3 { - padding-top: 16px !important; -} - -.pr-3 { - padding-right: 16px !important; -} - -.pb-3 { - padding-bottom: 16px !important; -} - -.pl-3 { - padding-left: 16px !important; -} - -.px-3 { - padding-right: 16px !important; - padding-left: 16px !important; -} - -.py-3 { - padding-top: 16px !important; - padding-bottom: 16px !important; -} - -.p-4 { - padding: 24px !important; -} - -.pt-4 { - padding-top: 24px !important; -} - -.pr-4 { - padding-right: 24px !important; -} - -.pb-4 { - padding-bottom: 24px !important; -} - -.pl-4 { - padding-left: 24px !important; -} - -.px-4 { - padding-right: 24px !important; - padding-left: 24px !important; -} - -.py-4 { - padding-top: 24px !important; - padding-bottom: 24px !important; -} - -.p-5 { - padding: 32px !important; -} - -.pt-5 { - padding-top: 32px !important; -} - -.pr-5 { - padding-right: 32px !important; -} - -.pb-5 { - padding-bottom: 32px !important; -} - -.pl-5 { - padding-left: 32px !important; -} - -.px-5 { - padding-right: 32px !important; - padding-left: 32px !important; -} - -.py-5 { - padding-top: 32px !important; - padding-bottom: 32px !important; -} - -.p-6 { - padding: 40px !important; -} - -.pt-6 { - padding-top: 40px !important; -} - -.pr-6 { - padding-right: 40px !important; -} - -.pb-6 { - padding-bottom: 40px !important; -} - -.pl-6 { - padding-left: 40px !important; -} - -.px-6 { - padding-right: 40px !important; - padding-left: 40px !important; -} - -.py-6 { - padding-top: 40px !important; - padding-bottom: 40px !important; -} - -@media (min-width: 544px) { - .p-sm-0 { - padding: 0 !important; - } - - .pt-sm-0 { - padding-top: 0 !important; - } - - .pr-sm-0 { - padding-right: 0 !important; - } - - .pb-sm-0 { - padding-bottom: 0 !important; - } - - .pl-sm-0 { - padding-left: 0 !important; - } - - .px-sm-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .py-sm-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .p-sm-1 { - padding: 4px !important; - } - - .pt-sm-1 { - padding-top: 4px !important; - } - - .pr-sm-1 { - padding-right: 4px !important; - } - - .pb-sm-1 { - padding-bottom: 4px !important; - } - - .pl-sm-1 { - padding-left: 4px !important; - } - - .px-sm-1 { - padding-right: 4px !important; - padding-left: 4px !important; - } - - .py-sm-1 { - padding-top: 4px !important; - padding-bottom: 4px !important; - } - - .p-sm-2 { - padding: 8px !important; - } - - .pt-sm-2 { - padding-top: 8px !important; - } - - .pr-sm-2 { - padding-right: 8px !important; - } - - .pb-sm-2 { - padding-bottom: 8px !important; - } - - .pl-sm-2 { - padding-left: 8px !important; - } - - .px-sm-2 { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .py-sm-2 { - padding-top: 8px !important; - padding-bottom: 8px !important; - } - - .p-sm-3 { - padding: 16px !important; - } - - .pt-sm-3 { - padding-top: 16px !important; - } - - .pr-sm-3 { - padding-right: 16px !important; - } - - .pb-sm-3 { - padding-bottom: 16px !important; - } - - .pl-sm-3 { - padding-left: 16px !important; - } - - .px-sm-3 { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .py-sm-3 { - padding-top: 16px !important; - padding-bottom: 16px !important; - } - - .p-sm-4 { - padding: 24px !important; - } - - .pt-sm-4 { - padding-top: 24px !important; - } - - .pr-sm-4 { - padding-right: 24px !important; - } - - .pb-sm-4 { - padding-bottom: 24px !important; - } - - .pl-sm-4 { - padding-left: 24px !important; - } - - .px-sm-4 { - padding-right: 24px !important; - padding-left: 24px !important; - } - - .py-sm-4 { - padding-top: 24px !important; - padding-bottom: 24px !important; - } - - .p-sm-5 { - padding: 32px !important; - } - - .pt-sm-5 { - padding-top: 32px !important; - } - - .pr-sm-5 { - padding-right: 32px !important; - } - - .pb-sm-5 { - padding-bottom: 32px !important; - } - - .pl-sm-5 { - padding-left: 32px !important; - } - - .px-sm-5 { - padding-right: 32px !important; - padding-left: 32px !important; - } - - .py-sm-5 { - padding-top: 32px !important; - padding-bottom: 32px !important; - } - - .p-sm-6 { - padding: 40px !important; - } - - .pt-sm-6 { - padding-top: 40px !important; - } - - .pr-sm-6 { - padding-right: 40px !important; - } - - .pb-sm-6 { - padding-bottom: 40px !important; - } - - .pl-sm-6 { - padding-left: 40px !important; - } - - .px-sm-6 { - padding-right: 40px !important; - padding-left: 40px !important; - } - - .py-sm-6 { - padding-top: 40px !important; - padding-bottom: 40px !important; - } -} - -@media (min-width: 768px) { - .p-md-0 { - padding: 0 !important; - } - - .pt-md-0 { - padding-top: 0 !important; - } - - .pr-md-0 { - padding-right: 0 !important; - } - - .pb-md-0 { - padding-bottom: 0 !important; - } - - .pl-md-0 { - padding-left: 0 !important; - } - - .px-md-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .py-md-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .p-md-1 { - padding: 4px !important; - } - - .pt-md-1 { - padding-top: 4px !important; - } - - .pr-md-1 { - padding-right: 4px !important; - } - - .pb-md-1 { - padding-bottom: 4px !important; - } - - .pl-md-1 { - padding-left: 4px !important; - } - - .px-md-1 { - padding-right: 4px !important; - padding-left: 4px !important; - } - - .py-md-1 { - padding-top: 4px !important; - padding-bottom: 4px !important; - } - - .p-md-2 { - padding: 8px !important; - } - - .pt-md-2 { - padding-top: 8px !important; - } - - .pr-md-2 { - padding-right: 8px !important; - } - - .pb-md-2 { - padding-bottom: 8px !important; - } - - .pl-md-2 { - padding-left: 8px !important; - } - - .px-md-2 { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .py-md-2 { - padding-top: 8px !important; - padding-bottom: 8px !important; - } - - .p-md-3 { - padding: 16px !important; - } - - .pt-md-3 { - padding-top: 16px !important; - } - - .pr-md-3 { - padding-right: 16px !important; - } - - .pb-md-3 { - padding-bottom: 16px !important; - } - - .pl-md-3 { - padding-left: 16px !important; - } - - .px-md-3 { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .py-md-3 { - padding-top: 16px !important; - padding-bottom: 16px !important; - } - - .p-md-4 { - padding: 24px !important; - } - - .pt-md-4 { - padding-top: 24px !important; - } - - .pr-md-4 { - padding-right: 24px !important; - } - - .pb-md-4 { - padding-bottom: 24px !important; - } - - .pl-md-4 { - padding-left: 24px !important; - } - - .px-md-4 { - padding-right: 24px !important; - padding-left: 24px !important; - } - - .py-md-4 { - padding-top: 24px !important; - padding-bottom: 24px !important; - } - - .p-md-5 { - padding: 32px !important; - } - - .pt-md-5 { - padding-top: 32px !important; - } - - .pr-md-5 { - padding-right: 32px !important; - } - - .pb-md-5 { - padding-bottom: 32px !important; - } - - .pl-md-5 { - padding-left: 32px !important; - } - - .px-md-5 { - padding-right: 32px !important; - padding-left: 32px !important; - } - - .py-md-5 { - padding-top: 32px !important; - padding-bottom: 32px !important; - } - - .p-md-6 { - padding: 40px !important; - } - - .pt-md-6 { - padding-top: 40px !important; - } - - .pr-md-6 { - padding-right: 40px !important; - } - - .pb-md-6 { - padding-bottom: 40px !important; - } - - .pl-md-6 { - padding-left: 40px !important; - } - - .px-md-6 { - padding-right: 40px !important; - padding-left: 40px !important; - } - - .py-md-6 { - padding-top: 40px !important; - padding-bottom: 40px !important; - } -} - -@media (min-width: 1012px) { - .p-lg-0 { - padding: 0 !important; - } - - .pt-lg-0 { - padding-top: 0 !important; - } - - .pr-lg-0 { - padding-right: 0 !important; - } - - .pb-lg-0 { - padding-bottom: 0 !important; - } - - .pl-lg-0 { - padding-left: 0 !important; - } - - .px-lg-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .py-lg-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .p-lg-1 { - padding: 4px !important; - } - - .pt-lg-1 { - padding-top: 4px !important; - } - - .pr-lg-1 { - padding-right: 4px !important; - } - - .pb-lg-1 { - padding-bottom: 4px !important; - } - - .pl-lg-1 { - padding-left: 4px !important; - } - - .px-lg-1 { - padding-right: 4px !important; - padding-left: 4px !important; - } - - .py-lg-1 { - padding-top: 4px !important; - padding-bottom: 4px !important; - } - - .p-lg-2 { - padding: 8px !important; - } - - .pt-lg-2 { - padding-top: 8px !important; - } - - .pr-lg-2 { - padding-right: 8px !important; - } - - .pb-lg-2 { - padding-bottom: 8px !important; - } - - .pl-lg-2 { - padding-left: 8px !important; - } - - .px-lg-2 { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .py-lg-2 { - padding-top: 8px !important; - padding-bottom: 8px !important; - } - - .p-lg-3 { - padding: 16px !important; - } - - .pt-lg-3 { - padding-top: 16px !important; - } - - .pr-lg-3 { - padding-right: 16px !important; - } - - .pb-lg-3 { - padding-bottom: 16px !important; - } - - .pl-lg-3 { - padding-left: 16px !important; - } - - .px-lg-3 { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .py-lg-3 { - padding-top: 16px !important; - padding-bottom: 16px !important; - } - - .p-lg-4 { - padding: 24px !important; - } - - .pt-lg-4 { - padding-top: 24px !important; - } - - .pr-lg-4 { - padding-right: 24px !important; - } - - .pb-lg-4 { - padding-bottom: 24px !important; - } - - .pl-lg-4 { - padding-left: 24px !important; - } - - .px-lg-4 { - padding-right: 24px !important; - padding-left: 24px !important; - } - - .py-lg-4 { - padding-top: 24px !important; - padding-bottom: 24px !important; - } - - .p-lg-5 { - padding: 32px !important; - } - - .pt-lg-5 { - padding-top: 32px !important; - } - - .pr-lg-5 { - padding-right: 32px !important; - } - - .pb-lg-5 { - padding-bottom: 32px !important; - } - - .pl-lg-5 { - padding-left: 32px !important; - } - - .px-lg-5 { - padding-right: 32px !important; - padding-left: 32px !important; - } - - .py-lg-5 { - padding-top: 32px !important; - padding-bottom: 32px !important; - } - - .p-lg-6 { - padding: 40px !important; - } - - .pt-lg-6 { - padding-top: 40px !important; - } - - .pr-lg-6 { - padding-right: 40px !important; - } - - .pb-lg-6 { - padding-bottom: 40px !important; - } - - .pl-lg-6 { - padding-left: 40px !important; - } - - .px-lg-6 { - padding-right: 40px !important; - padding-left: 40px !important; - } - - .py-lg-6 { - padding-top: 40px !important; - padding-bottom: 40px !important; - } -} - -@media (min-width: 1280px) { - .p-xl-0 { - padding: 0 !important; - } - - .pt-xl-0 { - padding-top: 0 !important; - } - - .pr-xl-0 { - padding-right: 0 !important; - } - - .pb-xl-0 { - padding-bottom: 0 !important; - } - - .pl-xl-0 { - padding-left: 0 !important; - } - - .px-xl-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .py-xl-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .p-xl-1 { - padding: 4px !important; - } - - .pt-xl-1 { - padding-top: 4px !important; - } - - .pr-xl-1 { - padding-right: 4px !important; - } - - .pb-xl-1 { - padding-bottom: 4px !important; - } - - .pl-xl-1 { - padding-left: 4px !important; - } - - .px-xl-1 { - padding-right: 4px !important; - padding-left: 4px !important; - } - - .py-xl-1 { - padding-top: 4px !important; - padding-bottom: 4px !important; - } - - .p-xl-2 { - padding: 8px !important; - } - - .pt-xl-2 { - padding-top: 8px !important; - } - - .pr-xl-2 { - padding-right: 8px !important; - } - - .pb-xl-2 { - padding-bottom: 8px !important; - } - - .pl-xl-2 { - padding-left: 8px !important; - } - - .px-xl-2 { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .py-xl-2 { - padding-top: 8px !important; - padding-bottom: 8px !important; - } - - .p-xl-3 { - padding: 16px !important; - } - - .pt-xl-3 { - padding-top: 16px !important; - } - - .pr-xl-3 { - padding-right: 16px !important; - } - - .pb-xl-3 { - padding-bottom: 16px !important; - } - - .pl-xl-3 { - padding-left: 16px !important; - } - - .px-xl-3 { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .py-xl-3 { - padding-top: 16px !important; - padding-bottom: 16px !important; - } - - .p-xl-4 { - padding: 24px !important; - } - - .pt-xl-4 { - padding-top: 24px !important; - } - - .pr-xl-4 { - padding-right: 24px !important; - } - - .pb-xl-4 { - padding-bottom: 24px !important; - } - - .pl-xl-4 { - padding-left: 24px !important; - } - - .px-xl-4 { - padding-right: 24px !important; - padding-left: 24px !important; - } - - .py-xl-4 { - padding-top: 24px !important; - padding-bottom: 24px !important; - } - - .p-xl-5 { - padding: 32px !important; - } - - .pt-xl-5 { - padding-top: 32px !important; - } - - .pr-xl-5 { - padding-right: 32px !important; - } - - .pb-xl-5 { - padding-bottom: 32px !important; - } - - .pl-xl-5 { - padding-left: 32px !important; - } - - .px-xl-5 { - padding-right: 32px !important; - padding-left: 32px !important; - } - - .py-xl-5 { - padding-top: 32px !important; - padding-bottom: 32px !important; - } - - .p-xl-6 { - padding: 40px !important; - } - - .pt-xl-6 { - padding-top: 40px !important; - } - - .pr-xl-6 { - padding-right: 40px !important; - } - - .pb-xl-6 { - padding-bottom: 40px !important; - } - - .pl-xl-6 { - padding-left: 40px !important; - } - - .px-xl-6 { - padding-right: 40px !important; - padding-left: 40px !important; - } - - .py-xl-6 { - padding-top: 40px !important; - padding-bottom: 40px !important; - } -} - -.p-responsive { - padding-right: 16px !important; - padding-left: 16px !important; -} - -@media (min-width: 544px) { - .p-responsive { - padding-right: 40px !important; - padding-left: 40px !important; - } -} - -@media (min-width: 1012px) { - .p-responsive { - padding-right: 16px !important; - padding-left: 16px !important; - } -} - -.h1 { - font-size: 26px !important; -} - -@media (min-width: 768px) { - .h1 { - font-size: 32px !important; - } -} - -.h2 { - font-size: 22px !important; -} - -@media (min-width: 768px) { - .h2 { - font-size: 24px !important; - } -} - -.h3 { - font-size: 18px !important; -} - -@media (min-width: 768px) { - .h3 { - font-size: 20px !important; - } -} - -.h4 { - font-size: 16px !important; -} - -.h5 { - font-size: 14px !important; -} - -.h6 { - font-size: 12px !important; -} - -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { - font-weight: 600 !important; -} - -.f1 { - font-size: 26px !important; -} - -@media (min-width: 768px) { - .f1 { - font-size: 32px !important; - } -} - -.f2 { - font-size: 22px !important; -} - -@media (min-width: 768px) { - .f2 { - font-size: 24px !important; - } -} - -.f3 { - font-size: 18px !important; -} - -@media (min-width: 768px) { - .f3 { - font-size: 20px !important; - } -} - -.f4 { - font-size: 16px !important; -} - -@media (min-width: 768px) { - .f4 { - font-size: 16px !important; - } -} - -.f5 { - font-size: 14px !important; -} - -.f6 { - font-size: 12px !important; -} - -.f00-light { - font-size: 40px !important; - font-weight: 300 !important; -} - -@media (min-width: 768px) { - .f00-light { - font-size: 48px !important; - } -} - -.f0-light { - font-size: 32px !important; - font-weight: 300 !important; -} - -@media (min-width: 768px) { - .f0-light { - font-size: 40px !important; - } -} - -.f1-light { - font-size: 26px !important; - font-weight: 300 !important; -} - -@media (min-width: 768px) { - .f1-light { - font-size: 32px !important; - } -} - -.f2-light { - font-size: 22px !important; - font-weight: 300 !important; -} - -@media (min-width: 768px) { - .f2-light { - font-size: 24px !important; - } -} - -.f3-light { - font-size: 18px !important; - font-weight: 300 !important; -} - -@media (min-width: 768px) { - .f3-light { - font-size: 20px !important; - } -} - -.text-small { - font-size: 12px !important; -} - -.lead { - margin-bottom: 30px; - font-size: 20px; - font-weight: 300; - color: #586069; -} - -.lh-condensed-ultra { - line-height: 1 !important; -} - -.lh-condensed { - line-height: 1.25 !important; -} - -.lh-default { - line-height: 1.5 !important; -} - -.lh-0 { - line-height: 0 !important; -} - -.text-right { - text-align: right !important; -} - -.text-left { - text-align: left !important; -} - -.text-center { - text-align: center !important; -} - -@media (min-width: 544px) { - .text-sm-right { - text-align: right !important; - } - - .text-sm-left { - text-align: left !important; - } - - .text-sm-center { - text-align: center !important; - } -} - -@media (min-width: 768px) { - .text-md-right { - text-align: right !important; - } - - .text-md-left { - text-align: left !important; - } - - .text-md-center { - text-align: center !important; - } -} - -@media (min-width: 1012px) { - .text-lg-right { - text-align: right !important; - } - - .text-lg-left { - text-align: left !important; - } - - .text-lg-center { - text-align: center !important; - } -} - -@media (min-width: 1280px) { - .text-xl-right { - text-align: right !important; - } - - .text-xl-left { - text-align: left !important; - } - - .text-xl-center { - text-align: center !important; - } -} - -.text-normal { - font-weight: 400 !important; -} - -.text-bold { - font-weight: 600 !important; -} - -.text-italic { - font-style: italic !important; -} - -.text-uppercase { - text-transform: uppercase !important; -} - -.text-underline { - text-decoration: underline !important; -} - -.no-underline { - text-decoration: none !important; -} - -.no-wrap { - white-space: nowrap !important; -} - -.ws-normal { - white-space: normal !important; -} - -.wb-break-all { - word-break: break-all !important; -} - -.text-emphasized { - font-weight: 600; - color: #24292e; -} - -.list-style-none { - list-style: none !important; -} - -.text-shadow-dark { - text-shadow: - 0 1px 1px rgba(27, 31, 35, 0.25), - 0 1px 25px rgba(27, 31, 35, 0.75); -} - -.text-shadow-light { - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} - -.text-mono { - font-family: - "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; -} - -.user-select-none { - user-select: none !important; -} - -.d-block { - display: block !important; -} - -.d-flex { - display: flex !important; -} - -.d-inline { - display: inline !important; -} - -.d-inline-block { - display: inline-block !important; -} - -.d-inline-flex { - display: inline-flex !important; -} - -.d-none { - display: none !important; -} - -.d-table { - display: table !important; -} - -.d-table-cell { - display: table-cell !important; -} - -@media (min-width: 544px) { - .d-sm-block { - display: block !important; - } - - .d-sm-flex { - display: flex !important; - } - - .d-sm-inline { - display: inline !important; - } - - .d-sm-inline-block { - display: inline-block !important; - } - - .d-sm-inline-flex { - display: inline-flex !important; - } - - .d-sm-none { - display: none !important; - } - - .d-sm-table { - display: table !important; - } - - .d-sm-table-cell { - display: table-cell !important; - } -} - -@media (min-width: 768px) { - .d-md-block { - display: block !important; - } - - .d-md-flex { - display: flex !important; - } - - .d-md-inline { - display: inline !important; - } - - .d-md-inline-block { - display: inline-block !important; - } - - .d-md-inline-flex { - display: inline-flex !important; - } - - .d-md-none { - display: none !important; - } - - .d-md-table { - display: table !important; - } - - .d-md-table-cell { - display: table-cell !important; - } -} - -@media (min-width: 1012px) { - .d-lg-block { - display: block !important; - } - - .d-lg-flex { - display: flex !important; - } - - .d-lg-inline { - display: inline !important; - } - - .d-lg-inline-block { - display: inline-block !important; - } - - .d-lg-inline-flex { - display: inline-flex !important; - } - - .d-lg-none { - display: none !important; - } - - .d-lg-table { - display: table !important; - } - - .d-lg-table-cell { - display: table-cell !important; - } -} - -@media (min-width: 1280px) { - .d-xl-block { - display: block !important; - } - - .d-xl-flex { - display: flex !important; - } - - .d-xl-inline { - display: inline !important; - } - - .d-xl-inline-block { - display: inline-block !important; - } - - .d-xl-inline-flex { - display: inline-flex !important; - } - - .d-xl-none { - display: none !important; - } - - .d-xl-table { - display: table !important; - } - - .d-xl-table-cell { - display: table-cell !important; - } -} - -.v-hidden { - visibility: hidden !important; -} - -.v-visible { - visibility: visible !important; -} - -@media (max-width: 544px) { - .hide-sm { - display: none !important; - } -} - -@media (min-width: 544px) and (max-width: 768px) { - .hide-md { - display: none !important; - } -} - -@media (min-width: 768px) and (max-width: 1012px) { - .hide-lg { - display: none !important; - } -} - -@media (min-width: 1012px) { - .hide-xl { - display: none !important; - } -} - -.table-fixed { - table-layout: fixed !important; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - word-wrap: normal; - border: 0; -} - -.show-on-focus { - position: absolute; - width: 1px; - height: 1px; - margin: 0; - overflow: hidden; - clip: rect(1px, 1px, 1px, 1px); -} - -.show-on-focus:focus { - z-index: 20; - width: auto; - height: auto; - clip: auto; -} - -.container { - width: 980px; - margin-right: auto; - margin-left: auto; -} - -.container::before { - display: table; - content: ""; -} - -.container::after { - display: table; - clear: both; - content: ""; -} - -.container-md { - max-width: 768px; - margin-right: auto; - margin-left: auto; -} - -.container-lg { - max-width: 1012px; - margin-right: auto; - margin-left: auto; -} - -.container-xl { - max-width: 1280px; - margin-right: auto; - margin-left: auto; -} - -.columns { - margin-right: -10px; - margin-left: -10px; -} - -.columns::before { - display: table; - content: ""; -} - -.columns::after { - display: table; - clear: both; - content: ""; -} - -.column { - float: left; - padding-right: 10px; - padding-left: 10px; -} - -.one-third { - width: 33.333333%; -} - -.two-thirds { - width: 66.666667%; -} - -.one-fourth { - width: 25%; -} - -.one-half { - width: 50%; -} - -.three-fourths { - width: 75%; -} - -.one-fifth { - width: 20%; -} - -.four-fifths { - width: 80%; -} - -.centered { - display: block; - float: none; - margin-right: auto; - margin-left: auto; -} - -.col-1 { - width: 8.3333333333%; -} - -.col-2 { - width: 16.6666666667%; -} - -.col-3 { - width: 25%; -} - -.col-4 { - width: 33.3333333333%; -} - -.col-5 { - width: 41.6666666667%; -} - -.col-6 { - width: 50%; -} - -.col-7 { - width: 58.3333333333%; -} - -.col-8 { - width: 66.6666666667%; -} - -.col-9 { - width: 75%; -} - -.col-10 { - width: 83.3333333333%; -} - -.col-11 { - width: 91.6666666667%; -} - -.col-12 { - width: 100%; -} - -@media (min-width: 544px) { - .col-sm-1 { - width: 8.3333333333%; - } - - .col-sm-2 { - width: 16.6666666667%; - } - - .col-sm-3 { - width: 25%; - } - - .col-sm-4 { - width: 33.3333333333%; - } - - .col-sm-5 { - width: 41.6666666667%; - } - - .col-sm-6 { - width: 50%; - } - - .col-sm-7 { - width: 58.3333333333%; - } - - .col-sm-8 { - width: 66.6666666667%; - } - - .col-sm-9 { - width: 75%; - } - - .col-sm-10 { - width: 83.3333333333%; - } - - .col-sm-11 { - width: 91.6666666667%; - } - - .col-sm-12 { - width: 100%; - } -} - -@media (min-width: 768px) { - .col-md-1 { - width: 8.3333333333%; - } - - .col-md-2 { - width: 16.6666666667%; - } - - .col-md-3 { - width: 25%; - } - - .col-md-4 { - width: 33.3333333333%; - } - - .col-md-5 { - width: 41.6666666667%; - } - - .col-md-6 { - width: 50%; - } - - .col-md-7 { - width: 58.3333333333%; - } - - .col-md-8 { - width: 66.6666666667%; - } - - .col-md-9 { - width: 75%; - } - - .col-md-10 { - width: 83.3333333333%; - } - - .col-md-11 { - width: 91.6666666667%; - } - - .col-md-12 { - width: 100%; - } -} - -@media (min-width: 1012px) { - .col-lg-1 { - width: 8.3333333333%; - } - - .col-lg-2 { - width: 16.6666666667%; - } - - .col-lg-3 { - width: 25%; - } - - .col-lg-4 { - width: 33.3333333333%; - } - - .col-lg-5 { - width: 41.6666666667%; - } - - .col-lg-6 { - width: 50%; - } - - .col-lg-7 { - width: 58.3333333333%; - } - - .col-lg-8 { - width: 66.6666666667%; - } - - .col-lg-9 { - width: 75%; - } - - .col-lg-10 { - width: 83.3333333333%; - } - - .col-lg-11 { - width: 91.6666666667%; - } - - .col-lg-12 { - width: 100%; - } -} - -@media (min-width: 1280px) { - .col-xl-1 { - width: 8.3333333333%; - } - - .col-xl-2 { - width: 16.6666666667%; - } - - .col-xl-3 { - width: 25%; - } - - .col-xl-4 { - width: 33.3333333333%; - } - - .col-xl-5 { - width: 41.6666666667%; - } - - .col-xl-6 { - width: 50%; - } - - .col-xl-7 { - width: 58.3333333333%; - } - - .col-xl-8 { - width: 66.6666666667%; - } - - .col-xl-9 { - width: 75%; - } - - .col-xl-10 { - width: 83.3333333333%; - } - - .col-xl-11 { - width: 91.6666666667%; - } - - .col-xl-12 { - width: 100%; - } -} - -.gutter { - margin-right: -16px; - margin-left: -16px; -} - -.gutter > [class*="col-"] { - padding-right: 16px !important; - padding-left: 16px !important; -} - -.gutter-condensed { - margin-right: -8px; - margin-left: -8px; -} - -.gutter-condensed > [class*="col-"] { - padding-right: 8px !important; - padding-left: 8px !important; -} - -.gutter-spacious { - margin-right: -24px; - margin-left: -24px; -} - -.gutter-spacious > [class*="col-"] { - padding-right: 24px !important; - padding-left: 24px !important; -} - -@media (min-width: 544px) { - .gutter-sm { - margin-right: -16px; - margin-left: -16px; - } - - .gutter-sm > [class*="col-"] { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .gutter-sm-condensed { - margin-right: -8px; - margin-left: -8px; - } - - .gutter-sm-condensed > [class*="col-"] { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .gutter-sm-spacious { - margin-right: -24px; - margin-left: -24px; - } - - .gutter-sm-spacious > [class*="col-"] { - padding-right: 24px !important; - padding-left: 24px !important; - } -} - -@media (min-width: 768px) { - .gutter-md { - margin-right: -16px; - margin-left: -16px; - } - - .gutter-md > [class*="col-"] { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .gutter-md-condensed { - margin-right: -8px; - margin-left: -8px; - } - - .gutter-md-condensed > [class*="col-"] { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .gutter-md-spacious { - margin-right: -24px; - margin-left: -24px; - } - - .gutter-md-spacious > [class*="col-"] { - padding-right: 24px !important; - padding-left: 24px !important; - } -} - -@media (min-width: 1012px) { - .gutter-lg { - margin-right: -16px; - margin-left: -16px; - } - - .gutter-lg > [class*="col-"] { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .gutter-lg-condensed { - margin-right: -8px; - margin-left: -8px; - } - - .gutter-lg-condensed > [class*="col-"] { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .gutter-lg-spacious { - margin-right: -24px; - margin-left: -24px; - } - - .gutter-lg-spacious > [class*="col-"] { - padding-right: 24px !important; - padding-left: 24px !important; - } -} - -@media (min-width: 1280px) { - .gutter-xl { - margin-right: -16px; - margin-left: -16px; - } - - .gutter-xl > [class*="col-"] { - padding-right: 16px !important; - padding-left: 16px !important; - } - - .gutter-xl-condensed { - margin-right: -8px; - margin-left: -8px; - } - - .gutter-xl-condensed > [class*="col-"] { - padding-right: 8px !important; - padding-left: 8px !important; - } - - .gutter-xl-spacious { - margin-right: -24px; - margin-left: -24px; - } - - .gutter-xl-spacious > [class*="col-"] { - padding-right: 24px !important; - padding-left: 24px !important; - } -} - -.offset-1 { - margin-left: 8.3333333333% !important; -} - -.offset-2 { - margin-left: 16.6666666667% !important; -} - -.offset-3 { - margin-left: 25% !important; -} - -.offset-4 { - margin-left: 33.3333333333% !important; -} - -.offset-5 { - margin-left: 41.6666666667% !important; -} - -.offset-6 { - margin-left: 50% !important; -} - -.offset-7 { - margin-left: 58.3333333333% !important; -} - -.offset-8 { - margin-left: 66.6666666667% !important; -} - -.offset-9 { - margin-left: 75% !important; -} - -.offset-10 { - margin-left: 83.3333333333% !important; -} - -.offset-11 { - margin-left: 91.6666666667% !important; -} - -@media (min-width: 544px) { - .offset-sm-1 { - margin-left: 8.3333333333% !important; - } - - .offset-sm-2 { - margin-left: 16.6666666667% !important; - } - - .offset-sm-3 { - margin-left: 25% !important; - } - - .offset-sm-4 { - margin-left: 33.3333333333% !important; - } - - .offset-sm-5 { - margin-left: 41.6666666667% !important; - } - - .offset-sm-6 { - margin-left: 50% !important; - } - - .offset-sm-7 { - margin-left: 58.3333333333% !important; - } - - .offset-sm-8 { - margin-left: 66.6666666667% !important; - } - - .offset-sm-9 { - margin-left: 75% !important; - } - - .offset-sm-10 { - margin-left: 83.3333333333% !important; - } - - .offset-sm-11 { - margin-left: 91.6666666667% !important; - } -} - -@media (min-width: 768px) { - .offset-md-1 { - margin-left: 8.3333333333% !important; - } - - .offset-md-2 { - margin-left: 16.6666666667% !important; - } - - .offset-md-3 { - margin-left: 25% !important; - } - - .offset-md-4 { - margin-left: 33.3333333333% !important; - } - - .offset-md-5 { - margin-left: 41.6666666667% !important; - } - - .offset-md-6 { - margin-left: 50% !important; - } - - .offset-md-7 { - margin-left: 58.3333333333% !important; - } - - .offset-md-8 { - margin-left: 66.6666666667% !important; - } - - .offset-md-9 { - margin-left: 75% !important; - } - - .offset-md-10 { - margin-left: 83.3333333333% !important; - } - - .offset-md-11 { - margin-left: 91.6666666667% !important; - } -} - -@media (min-width: 1012px) { - .offset-lg-1 { - margin-left: 8.3333333333% !important; - } - - .offset-lg-2 { - margin-left: 16.6666666667% !important; - } - - .offset-lg-3 { - margin-left: 25% !important; - } - - .offset-lg-4 { - margin-left: 33.3333333333% !important; - } - - .offset-lg-5 { - margin-left: 41.6666666667% !important; - } - - .offset-lg-6 { - margin-left: 50% !important; - } - - .offset-lg-7 { - margin-left: 58.3333333333% !important; - } - - .offset-lg-8 { - margin-left: 66.6666666667% !important; - } - - .offset-lg-9 { - margin-left: 75% !important; - } - - .offset-lg-10 { - margin-left: 83.3333333333% !important; - } - - .offset-lg-11 { - margin-left: 91.6666666667% !important; - } -} - -@media (min-width: 1280px) { - .offset-xl-1 { - margin-left: 8.3333333333% !important; - } - - .offset-xl-2 { - margin-left: 16.6666666667% !important; - } - - .offset-xl-3 { - margin-left: 25% !important; - } - - .offset-xl-4 { - margin-left: 33.3333333333% !important; - } - - .offset-xl-5 { - margin-left: 41.6666666667% !important; - } - - .offset-xl-6 { - margin-left: 50% !important; - } - - .offset-xl-7 { - margin-left: 58.3333333333% !important; - } - - .offset-xl-8 { - margin-left: 66.6666666667% !important; - } - - .offset-xl-9 { - margin-left: 75% !important; - } - - .offset-xl-10 { - margin-left: 83.3333333333% !important; - } - - .offset-xl-11 { - margin-left: 91.6666666667% !important; - } -} - -.markdown-body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - font-size: 16px; - line-height: 1.5; - word-wrap: break-word; -} - -.markdown-body::before { - display: table; - content: ""; -} - -.markdown-body::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body > *:first-child { - margin-top: 0 !important; -} - -.markdown-body > *:last-child { - margin-bottom: 0 !important; -} - -.markdown-body a:not([href]) { - color: inherit; - text-decoration: none; -} - -.markdown-body .absent { - color: #cb2431; -} - -.markdown-body .anchor { - float: left; - padding-right: 4px; - margin-left: -20px; - line-height: 1; -} - -.markdown-body .anchor:focus { - outline: none; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre { - margin-top: 0; - margin-bottom: 16px; -} - -.markdown-body hr { - height: 0.25em; - padding: 0; - margin: 24px 0; - background-color: #e1e4e8; - border: 0; -} - -.markdown-body blockquote { - padding: 0 1em; - color: #6a737d; - border-left: 0.25em solid #dfe2e5; -} - -.markdown-body blockquote > :first-child { - margin-top: 0; -} - -.markdown-body blockquote > :last-child { - margin-bottom: 0; -} - -.markdown-body kbd { - display: inline-block; - padding: 3px 5px; - font-size: 11px; - line-height: 10px; - color: #444d56; - vertical-align: middle; - background-color: #fafbfc; - border: solid 1px #c6cbd1; - border-bottom-color: #959da5; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #959da5; -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; -} - -.markdown-body h1 .octicon-link, -.markdown-body h2 .octicon-link, -.markdown-body h3 .octicon-link, -.markdown-body h4 .octicon-link, -.markdown-body h5 .octicon-link, -.markdown-body h6 .octicon-link { - color: #1b1f23; - vertical-align: middle; - visibility: hidden; -} - -.markdown-body h1:hover .anchor, -.markdown-body h2:hover .anchor, -.markdown-body h3:hover .anchor, -.markdown-body h4:hover .anchor, -.markdown-body h5:hover .anchor, -.markdown-body h6:hover .anchor { - text-decoration: none; -} - -.markdown-body h1:hover .anchor .octicon-link, -.markdown-body h2:hover .anchor .octicon-link, -.markdown-body h3:hover .anchor .octicon-link, -.markdown-body h4:hover .anchor .octicon-link, -.markdown-body h5:hover .anchor .octicon-link, -.markdown-body h6:hover .anchor .octicon-link { - visibility: visible; -} - -.markdown-body h1 tt, -.markdown-body h1 code, -.markdown-body h2 tt, -.markdown-body h2 code, -.markdown-body h3 tt, -.markdown-body h3 code, -.markdown-body h4 tt, -.markdown-body h4 code, -.markdown-body h5 tt, -.markdown-body h5 code, -.markdown-body h6 tt, -.markdown-body h6 code { - font-size: inherit; -} - -.markdown-body h1 { - padding-bottom: 0.3em; - font-size: 2em; - border-bottom: 1px solid #eaecef; -} - -.markdown-body h2 { - padding-bottom: 0.3em; - font-size: 1.5em; - border-bottom: 1px solid #eaecef; -} - -.markdown-body h3 { - font-size: 1.25em; -} - -.markdown-body h4 { - font-size: 1em; -} - -.markdown-body h5 { - font-size: 0.875em; -} - -.markdown-body h6 { - font-size: 0.85em; - color: #6a737d; -} - -.markdown-body ul, -.markdown-body ol { - padding-left: 2em; -} - -.markdown-body ul.no-list, -.markdown-body ol.no-list { - padding: 0; - list-style-type: none; -} - -.markdown-body ul ul, -.markdown-body ul ol, -.markdown-body ol ol, -.markdown-body ol ul { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body li { - word-wrap: break-all; -} - -.markdown-body li > p { - margin-top: 16px; -} - -.markdown-body li + li { - margin-top: 0.25em; -} - -.markdown-body dl { - padding: 0; -} - -.markdown-body dl dt { - padding: 0; - margin-top: 16px; - font-size: 1em; - font-style: italic; - font-weight: 600; -} - -.markdown-body dl dd { - padding: 0 16px; - margin-bottom: 16px; -} - -.markdown-body table { - display: block; - width: 100%; - overflow: auto; -} - -.markdown-body table th { - font-weight: 600; -} - -.markdown-body table th, -.markdown-body table td { - padding: 6px 13px; - border: 1px solid #dfe2e5; -} - -.markdown-body table tr { - background-color: #fff; - border-top: 1px solid #c6cbd1; -} - -.markdown-body table tr:nth-child(2n) { - background-color: #f6f8fa; -} - -.markdown-body table img { - background-color: transparent; -} - -.markdown-body img { - max-width: 100%; - box-sizing: content-box; - background-color: #fff; -} - -.markdown-body img[align="right"] { - padding-left: 20px; -} - -.markdown-body img[align="left"] { - padding-right: 20px; -} - -.markdown-body .emoji { - max-width: none; - vertical-align: text-top; - background-color: transparent; -} - -.markdown-body span.frame { - display: block; - overflow: hidden; -} - -.markdown-body span.frame > span { - display: block; - float: left; - width: auto; - padding: 7px; - margin: 13px 0 0; - overflow: hidden; - border: 1px solid #dfe2e5; -} - -.markdown-body span.frame span img { - display: block; - float: left; -} - -.markdown-body span.frame span span { - display: block; - padding: 5px 0 0; - clear: both; - color: #24292e; -} - -.markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-center > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; -} - -.markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-right > span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markdown-body span.align-right span img { - margin: 0; - text-align: right; -} - -.markdown-body span.float-left { - display: block; - float: left; - margin-right: 13px; - overflow: hidden; -} - -.markdown-body span.float-left span { - margin: 13px 0 0; -} - -.markdown-body span.float-right { - display: block; - float: right; - margin-left: 13px; - overflow: hidden; -} - -.markdown-body span.float-right > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - -.markdown-body code, -.markdown-body tt { - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: rgba(27, 31, 35, 0.05); - border-radius: 3px; -} - -.markdown-body code br, -.markdown-body tt br { - display: none; -} - -.markdown-body del code { - text-decoration: inherit; -} - -.markdown-body pre { - word-wrap: normal; -} - -.markdown-body pre > code { - padding: 0; - margin: 0; - font-size: 100%; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} - -.markdown-body .highlight { - margin-bottom: 16px; -} - -.markdown-body .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.markdown-body .highlight pre, -.markdown-body pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #f6f8fa; - border-radius: 3px; -} - -.markdown-body pre code, -.markdown-body pre tt { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -.markdown-body .csv-data td, -.markdown-body .csv-data th { - padding: 5px; - overflow: hidden; - font-size: 12px; - line-height: 1; - text-align: left; - white-space: nowrap; -} - -.markdown-body .csv-data .blob-num { - padding: 10px 8px 9px; - text-align: right; - background: #fff; - border: 0; -} - -.markdown-body .csv-data tr { - border-top: 0; -} - -.markdown-body .csv-data th { - font-weight: 600; - background: #f6f8fa; - border-top: 0; -} - -.highlight table td { - padding: 5px; -} - -.highlight table pre { - margin: 0; -} - -.highlight .cm { - color: #999988; - font-style: italic; -} - -.highlight .cp { - color: #999999; - font-weight: bold; -} - -.highlight .c1 { - color: #999988; - font-style: italic; -} - -.highlight .cs { - color: #999999; - font-weight: bold; - font-style: italic; -} - -.highlight .c, -.highlight .cd { - color: #999988; - font-style: italic; -} - -.highlight .err { - color: #a61717; - background-color: #e3d2d2; -} - -.highlight .gd { - color: #000000; - background-color: #ffdddd; -} - -.highlight .ge { - color: #000000; - font-style: italic; -} - -.highlight .gr { - color: #aa0000; -} - -.highlight .gh { - color: #999999; -} - -.highlight .gi { - color: #000000; - background-color: #ddffdd; -} - -.highlight .go { - color: #888888; -} - -.highlight .gp { - color: #555555; -} - -.highlight .gs { - font-weight: bold; -} - -.highlight .gu { - color: #aaaaaa; -} - -.highlight .gt { - color: #aa0000; -} - -.highlight .kc { - color: #000000; - font-weight: bold; -} - -.highlight .kd { - color: #000000; - font-weight: bold; -} - -.highlight .kn { - color: #000000; - font-weight: bold; -} - -.highlight .kp { - color: #000000; - font-weight: bold; -} - -.highlight .kr { - color: #000000; - font-weight: bold; -} - -.highlight .kt { - color: #445588; - font-weight: bold; -} - -.highlight .k, -.highlight .kv { - color: #000000; - font-weight: bold; -} - -.highlight .mf { - color: #009999; -} - -.highlight .mh { - color: #009999; -} - -.highlight .il { - color: #009999; -} - -.highlight .mi { - color: #009999; -} - -.highlight .mo { - color: #009999; -} - -.highlight .m, -.highlight .mb, -.highlight .mx { - color: #009999; -} - -.highlight .sb { - color: #d14; -} - -.highlight .sc { - color: #d14; -} - -.highlight .sd { - color: #d14; -} - -.highlight .s2 { - color: #d14; -} - -.highlight .se { - color: #d14; -} - -.highlight .sh { - color: #d14; -} - -.highlight .si { - color: #d14; -} - -.highlight .sx { - color: #d14; -} - -.highlight .sr { - color: #009926; -} - -.highlight .s1 { - color: #d14; -} - -.highlight .ss { - color: #990073; -} - -.highlight .s { - color: #d14; -} - -.highlight .na { - color: #008080; -} - -.highlight .bp { - color: #999999; -} - -.highlight .nb { - color: #0086b3; -} - -.highlight .nc { - color: #445588; - font-weight: bold; -} - -.highlight .no { - color: #008080; -} - -.highlight .nd { - color: #3c5d5d; - font-weight: bold; -} - -.highlight .ni { - color: #800080; -} - -.highlight .ne { - color: #990000; - font-weight: bold; -} - -.highlight .nf { - color: #990000; - font-weight: bold; -} - -.highlight .nl { - color: #990000; - font-weight: bold; -} - -.highlight .nn { - color: #555555; -} - -.highlight .nt { - color: #000080; -} - -.highlight .vc { - color: #008080; -} - -.highlight .vg { - color: #008080; -} - -.highlight .vi { - color: #008080; -} - -.highlight .nv { - color: #008080; -} - -.highlight .ow { - color: #000000; - font-weight: bold; -} - -.highlight .o { - color: #000000; - font-weight: bold; -} - -.highlight .w { - color: #bbbbbb; -} - -.highlight { - background-color: #f8f8f8; -} diff --git a/docs/templates/base.html b/docs/templates/base.html deleted file mode 100644 index 848168c..0000000 --- a/docs/templates/base.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - {{ page_title }} – {{ site.site_name }} - - - - - - - - - - -

- - -
-
-

{{ page_title }}

- {{ page_content | safe }} -
- -
- - Built with librawssg - · Licensed under {{ site.license | default(value='MIT') }} - -
-
-
- - - - - diff --git a/docs/templates/rss.xml b/docs/templates/rss.xml deleted file mode 100644 index 92c45bf..0000000 --- a/docs/templates/rss.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - {{ site.site_name }} - {{ site.description }} - {{ site.base_url }} - - {{ site.language | default(value='en') }} - {% for post in posts %} - - {{ post.frontmatter.title }} - {{ site.base_url }}/{{ post.url }} - {{ post.frontmatter.desc }} - {{ post.pub_date }} - {{ site.base_url }}/{{ post.url }} - - {% endfor %} - - diff --git a/docs/templates/sitemap.xml b/docs/templates/sitemap.xml deleted file mode 100644 index ae3b004..0000000 --- a/docs/templates/sitemap.xml +++ /dev/null @@ -1,13 +0,0 @@ - - -{% for page in pages %} - - {{ site.base_url }}/{{ page.url }} - {% if page.pub_date %} - {{ page.pub_date }} - {% endif %} - monthly - 0.8 - -{% endfor %} - diff --git a/tests/common/mod.rs b/tests/common/mod.rs deleted file mode 100644 index da58718..0000000 --- a/tests/common/mod.rs +++ /dev/null @@ -1,227 +0,0 @@ -#![allow(dead_code, unused_imports)] - -use librawssg::config::ConfigLoader; -use librawssg::error::RawssgError; -use librawssg::fs::FileSystem; -use librawssg::markdown::MarkdownRenderer; -use librawssg::site::{Context, TemplateRenderer}; -use librawssg::types::RawssgConfig; -use std::collections::HashMap; -use std::io; -use std::path::{Path, PathBuf}; - -pub struct MockFs { - pub files: HashMap>, - pub dirs: Vec, - pub read_error: Option, -} - -impl MockFs { - pub fn new() -> Self { - Self { - files: HashMap::new(), - dirs: vec![], - read_error: None, - } - } - - pub fn add_file(&mut self, path: &str, content: &str) { - self.files - .insert(PathBuf::from(path), content.as_bytes().to_vec()); - if let Some(parent) = Path::new(path).parent() { - let mut p = PathBuf::new(); - for comp in parent.components() { - p.push(comp); - if !self.dirs.contains(&p) { - self.dirs.push(p.clone()); - } - } - } - } -} - -impl FileSystem for MockFs { - fn read_to_string(&self, path: &Path) -> io::Result { - if let Some(err_path) = &self.read_error - && err_path == path - { - return Err(io::Error::new(io::ErrorKind::NotFound, "mock error")); - } - self.files - .get(path) - .map(|b| String::from_utf8_lossy(b).to_string()) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found")) - } - - fn read_bytes(&self, path: &Path) -> io::Result> { - self.files - .get(path) - .cloned() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file not found")) - } - - fn write(&self, _path: &Path, _content: &[u8]) -> io::Result<()> { - Ok(()) - } - fn create_dir_all(&self, _path: &Path) -> io::Result<()> { - Ok(()) - } - fn remove_dir_all(&self, _path: &Path) -> io::Result<()> { - Ok(()) - } - - fn exists(&self, path: &Path) -> bool { - self.files.contains_key(path) || self.dirs.contains(&path.to_path_buf()) - } - - fn is_dir(&self, path: &Path) -> bool { - self.dirs.contains(&path.to_path_buf()) - } - fn is_file(&self, path: &Path) -> bool { - self.files.contains_key(path) - } - - fn read_dir(&self, path: &Path) -> io::Result> { - let mut entries = vec![]; - for f in self.files.keys() { - if let Some(parent) = f.parent() - && parent == path - { - entries.push(f.clone()); - } - } - for d in &self.dirs { - if let Some(parent) = d.parent() - && parent == path - { - entries.push(d.clone()); - } - } - Ok(entries) - } - - fn copy_file(&self, _from: &Path, _to: &Path) -> io::Result { - Ok(0) - } - - fn walk_dir(&self, root: &Path) -> io::Result> { - Ok(self - .files - .keys() - .filter(|p| p.starts_with(root)) - .cloned() - .collect()) - } - - fn canonicalize(&self, path: &Path) -> io::Result { - let absolute = if path.is_absolute() { - path.to_path_buf() - } else { - Path::new("/").join(path) - }; - - let mut components = Vec::new(); - for comp in absolute.components() { - match comp { - std::path::Component::ParentDir => { - components.pop(); - } - std::path::Component::CurDir => {} - other => components.push(other), - } - } - let canonical: PathBuf = components.iter().collect(); - - if self.files.contains_key(&canonical) || self.dirs.contains(&canonical) { - Ok(canonical) - } else { - Err(io::Error::new( - io::ErrorKind::NotFound, - "mock: path not found", - )) - } - } - - fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> { - Ok(()) - } -} - -pub struct MockMarkdownRenderer { - pub render_fn: Box String + Send + Sync>, -} - -impl MockMarkdownRenderer { - pub fn new String + Send + Sync + 'static>(f: F) -> Self { - Self { - render_fn: Box::new(f), - } - } - pub fn identity() -> Self { - Self::new(|s| s.to_string()) - } -} - -impl MarkdownRenderer for MockMarkdownRenderer { - fn render(&self, markdown: &str) -> String { - (self.render_fn)(markdown) - } -} - -#[allow(clippy::type_complexity)] -pub struct MockTemplateRenderer { - pub templates: HashMap, - pub render_fn: Box Result + Send + Sync>, -} - -impl MockTemplateRenderer { - pub fn new() -> Self { - Self { - templates: HashMap::new(), - render_fn: Box::new(|name, _ctx| Ok(format!("rendered:{}", name))), - } - } - - pub fn with_fn< - F: Fn(&str, &dyn Context) -> Result + Send + Sync + 'static, - >( - f: F, - ) -> Self { - Self { - templates: HashMap::new(), - render_fn: Box::new(f), - } - } - - pub fn add_raw_template(&mut self, name: &str, content: &str) { - self.templates.insert(name.to_string(), content.to_string()); - } -} - -impl TemplateRenderer for MockTemplateRenderer { - fn render(&self, template: &str, context: &dyn Context) -> Result { - (self.render_fn)(template, context) - } -} - -pub struct MockConfigLoader { - pub config_fn: Box Result + Send + Sync>, -} - -impl MockConfigLoader { - pub fn new Result + Send + Sync + 'static>(f: F) -> Self { - Self { - config_fn: Box::new(f), - } - } -} - -impl ConfigLoader for MockConfigLoader { - fn load(&self) -> Result { - (self.config_fn)() - } -} - -pub fn default_test_config() -> RawssgConfig { - RawssgConfig::default() -} diff --git a/tests/common_mocks.rs b/tests/common_mocks.rs deleted file mode 100644 index 8e088a9..0000000 --- a/tests/common_mocks.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![allow(dead_code)] - -#[cfg(feature = "tera")] -pub mod context_mocks { - use librawssg::error::RawssgError; - use librawssg::site::Context; - use librawssg::site::context::{FeedContextBuilder, SitemapContextBuilder}; - use librawssg::types::{PageContext, RawssgConfig}; - - pub struct MockFeedContextBuilder; - - impl FeedContextBuilder for MockFeedContextBuilder { - fn build_feed_context( - &self, - config: &RawssgConfig, - posts: &[&PageContext], - base_url: &str, - ) -> Result, RawssgError> { - let mut ctx = tera::Context::new(); - ctx.insert("site", &config.site); - ctx.insert("posts", posts); - ctx.insert("base_url", base_url); - Ok(Box::new(ctx)) - } - } - - pub struct MockSitemapContextBuilder; - - impl SitemapContextBuilder for MockSitemapContextBuilder { - fn build_sitemap_context( - &self, - config: &RawssgConfig, - pages: &[PageContext], - base_url: &str, - ) -> Result, RawssgError> { - let mut ctx = tera::Context::new(); - ctx.insert("site", &config.site); - ctx.insert("pages", pages); - ctx.insert("base_url", base_url); - Ok(Box::new(ctx)) - } - } -} diff --git a/tests/config_tests.rs b/tests/config_tests.rs deleted file mode 100644 index 93d77f1..0000000 --- a/tests/config_tests.rs +++ /dev/null @@ -1,89 +0,0 @@ -use librawssg::config::{ConfigLoader, loader::YamlConfigLoader}; -use librawssg::error::RawssgError; -use librawssg::types::RawssgConfig; -use std::io::Write; -use tempfile::NamedTempFile; - -#[test] -fn default_config_returns_valid_default() { - let loader = librawssg::config::loader::DefaultConfig; - let config = loader.load().expect("default config should load"); - assert_eq!(config.site.site_name, "rawssg"); - assert_eq!(config.build.content_dir, "content"); - assert!( - !config.content_types.is_empty(), - "default config must have at least one content type" - ); - assert_eq!(config.content_types.len(), 1); - assert_eq!(config.content_types[0].name, "page"); - assert_eq!(config.content_types[0].pattern, "**/*.md"); - assert_eq!(config.content_types[0].template, "base.html"); -} - -#[test] -fn yaml_loader_loads_full_config() { - let yaml = r#" -site: - site_name: "Test Site" - description: "A test" -build: - content_dir: "my_content" - output_dir: "public" -content_types: - - name: blog - pattern: "blog/*.md" - template: "post.html" -"#; - let mut file = NamedTempFile::new().unwrap(); - file.write_all(yaml.as_bytes()).unwrap(); - let loader = YamlConfigLoader::new(file.path()); - let config = loader.load().unwrap(); - assert_eq!(config.site.site_name, "Test Site"); - assert_eq!(config.build.content_dir, "my_content"); - assert_eq!(config.content_types.len(), 1); - assert_eq!(config.content_types[0].name, "blog"); -} - -#[test] -fn yaml_loader_partial_defaults() { - let yaml = "site:\n site_name: \"OnlyName\""; - let mut file = NamedTempFile::new().unwrap(); - file.write_all(yaml.as_bytes()).unwrap(); - let loader = YamlConfigLoader::new(file.path()); - let config = loader.load().unwrap(); - assert_eq!(config.site.site_name, "OnlyName"); - assert_eq!(config.build.content_dir, "content"); -} - -#[test] -fn yaml_loader_invalid_syntax_error() { - let yaml = "site: [unclosed"; - let mut file = NamedTempFile::new().unwrap(); - file.write_all(yaml.as_bytes()).unwrap(); - let loader = YamlConfigLoader::new(file.path()); - match loader.load() { - Err(RawssgError::Config(_)) => {} - _ => panic!("expected Config error"), - } -} - -#[test] -fn yaml_loader_file_not_found() { - let loader = YamlConfigLoader::new("nonexistent.yaml"); - match loader.load() { - Err(RawssgError::Config(_)) => {} - _ => panic!("expected Config error"), - } -} - -#[test] -fn config_loader_trait_load_or_default_on_error() { - struct FailingLoader; - impl ConfigLoader for FailingLoader { - fn load(&self) -> Result { - Err(RawssgError::Config("fail".into())) - } - } - let config = FailingLoader.load_or_default(); - assert_eq!(config.site.site_name, "rawssg"); -} diff --git a/tests/edge_cases_tests.rs b/tests/edge_cases_tests.rs deleted file mode 100644 index 9f76711..0000000 --- a/tests/edge_cases_tests.rs +++ /dev/null @@ -1,54 +0,0 @@ -use librawssg::config::{ConfigLoader, loader::YamlConfigLoader}; -use librawssg::error::RawssgError; -use librawssg::types::RawssgConfig; -use std::io::Write; -use tempfile::NamedTempFile; - -#[test] -fn load_or_default_uses_default_on_error() { - struct BadLoader; - impl ConfigLoader for BadLoader { - fn load(&self) -> Result { - Err(RawssgError::Internal("boom".into())) - } - } - let config = BadLoader.load_or_default(); - assert_eq!(config.site.site_name, "rawssg"); -} - -#[test] -fn load_or_default_returns_loaded_config_when_ok() { - struct GoodLoader; - impl ConfigLoader for GoodLoader { - fn load(&self) -> Result { - let mut cfg = RawssgConfig::default(); - cfg.site.site_name = "custom".into(); - Ok(cfg) - } - } - let config = GoodLoader.load_or_default(); - assert_eq!(config.site.site_name, "custom"); -} - -#[test] -fn yaml_loader_invalid_yaml_error() { - let yaml = "site:\n site_name: [invalid"; - let mut file = NamedTempFile::new().unwrap(); - file.write_all(yaml.as_bytes()).unwrap(); - let loader = YamlConfigLoader::new(file.path()); - let err = loader.load().unwrap_err(); - match err { - RawssgError::Config(msg) => assert!(msg.contains("invalid config YAML")), - _ => panic!("Expected Config error"), - } -} - -#[test] -fn yaml_loader_file_not_found_error() { - let loader = YamlConfigLoader::new("/nonexistent/path.yaml"); - let err = loader.load().unwrap_err(); - match err { - RawssgError::Config(msg) => assert!(msg.contains("cannot read config")), - _ => panic!("Expected Config error"), - } -} diff --git a/tests/error_tests.rs b/tests/error_tests.rs deleted file mode 100644 index 8db8364..0000000 --- a/tests/error_tests.rs +++ /dev/null @@ -1,16 +0,0 @@ -use librawssg::error::RawssgError; -use std::path::PathBuf; - -#[test] -fn error_display_and_diagnostic_code() { - let err = RawssgError::Frontmatter { - path: PathBuf::from("test.md"), - source: Box::new(std::io::Error::other("oops")), - }; - let msg = format!("{}", err); - assert!(msg.contains("Failed to parse frontmatter in test.md")); - assert_eq!( - format!("{:?}", err), - "Frontmatter { path: \"test.md\", source: Custom { kind: Other, error: \"oops\" } }" - ); -} diff --git a/tests/frontmatter_tests.rs b/tests/frontmatter_tests.rs deleted file mode 100644 index cae9f7c..0000000 --- a/tests/frontmatter_tests.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod common; -use common::MockMarkdownRenderer; -use librawssg::error::RawssgError; -use librawssg::frontmatter::parse_frontmatter_and_render; -use std::path::Path; - -#[test] -fn valid_frontmatter_parsed() { - let input = "---\ntitle: Test\ndesc: Desc\n---\n# Content"; - let renderer = MockMarkdownRenderer::identity(); - let (fm, html) = parse_frontmatter_and_render(input, Path::new("file.md"), &renderer).unwrap(); - assert_eq!(fm.title, "Test"); - assert_eq!(fm.desc, "Desc"); - assert_eq!(html, "# Content"); -} - -#[test] -fn missing_opening_dashes_error() { - let input = "# No frontmatter"; - let renderer = MockMarkdownRenderer::identity(); - let err = parse_frontmatter_and_render(input, Path::new("file.md"), &renderer).unwrap_err(); - match err { - RawssgError::Frontmatter { path, source } => { - assert!(source.to_string().contains("missing opening")); - assert_eq!(path, Path::new("file.md")); - } - _ => panic!("wrong error"), - } -} - -#[test] -fn invalid_yaml_in_frontmatter() { - let input = "---\ntitle: [broken\ndesc: x\n---\ncontent"; - let renderer = MockMarkdownRenderer::identity(); - assert!(parse_frontmatter_and_render(input, Path::new("f.md"), &renderer).is_err()); -} - -#[test] -fn missing_closing_dashes_error() { - let input = "---\ntitle: Test\ndesc: Test desc\n"; - let renderer = MockMarkdownRenderer::identity(); - let err = parse_frontmatter_and_render(input, Path::new("f.md"), &renderer).unwrap_err(); - match err { - RawssgError::Frontmatter { source, .. } => { - assert!(source.to_string().contains("missing closing '---'")); - } - _ => panic!("expected Frontmatter error about missing closing dashes"), - } -} - -#[test] -fn windows_line_endings() { - let input = "---\r\ntitle: Win\r\ndesc: Win desc\r\n---\r\n# Content"; - let renderer = MockMarkdownRenderer::identity(); - let (fm, html) = parse_frontmatter_and_render(input, Path::new("f.md"), &renderer).unwrap(); - assert_eq!(fm.title, "Win"); - assert_eq!(fm.desc, "Win desc"); - assert_eq!(html, "# Content"); -} - -#[test] -fn only_frontmatter_no_content() { - let input = "---\ntitle: Only\ndesc: Only desc\n---"; - let renderer = MockMarkdownRenderer::identity(); - let (fm, html) = parse_frontmatter_and_render(input, Path::new("f.md"), &renderer).unwrap(); - assert_eq!(fm.title, "Only"); - assert_eq!(fm.desc, "Only desc"); - assert!(html.is_empty()); -} - -#[test] -fn empty_yaml_values_still_valid() { - let input = "---\ntitle: \"\"\ndesc: \"\"\n---\n# Content after FM"; - let renderer = MockMarkdownRenderer::identity(); - let (fm, html) = parse_frontmatter_and_render(input, Path::new("f.md"), &renderer).unwrap(); - assert_eq!(fm.title, ""); - assert_eq!(fm.desc, ""); - assert_eq!(html, "# Content after FM"); -} diff --git a/tests/fs_mock_tests.rs b/tests/fs_mock_tests.rs deleted file mode 100644 index b605b8c..0000000 --- a/tests/fs_mock_tests.rs +++ /dev/null @@ -1,76 +0,0 @@ -mod common; -use common::MockFs; -use librawssg::fs::FileSystem; -use std::path::Path; - -#[test] -fn read_to_string_existing_file() { - let mut fs = MockFs::new(); - fs.add_file("/test.txt", "hello"); - let content = fs.read_to_string(Path::new("/test.txt")).unwrap(); - assert_eq!(content, "hello"); -} - -#[test] -fn read_to_string_not_found() { - let fs = MockFs::new(); - assert!(fs.read_to_string(Path::new("/missing.txt")).is_err()); -} - -#[test] -fn read_bytes_existing() { - let mut fs = MockFs::new(); - fs.add_file("/data.bin", "\x00\x01"); - let bytes = fs.read_bytes(Path::new("/data.bin")).unwrap(); - assert_eq!(bytes, vec![0, 1]); -} - -#[test] -fn exists_true() { - let mut fs = MockFs::new(); - fs.add_file("/file.txt", "x"); - assert!(fs.exists(Path::new("/file.txt"))); -} - -#[test] -fn exists_false() { - let fs = MockFs::new(); - assert!(!fs.exists(Path::new("/ghost.txt"))); -} - -#[test] -fn is_dir_and_is_file() { - let mut fs = MockFs::new(); - fs.add_file("/dir/file.txt", "data"); - assert!(fs.is_dir(Path::new("/dir"))); - assert!(fs.is_file(Path::new("/dir/file.txt"))); - assert!(!fs.is_dir(Path::new("/dir/file.txt"))); -} - -#[test] -fn read_dir_returns_entries() { - let mut fs = MockFs::new(); - fs.add_file("/root/a.md", "a"); - fs.add_file("/root/b.md", "b"); - fs.dirs.push(Path::new("/root/sub").to_path_buf()); - let entries = fs.read_dir(Path::new("/root")).unwrap(); - assert_eq!(entries.len(), 3); -} - -#[test] -fn walk_dir_recursive() { - let mut fs = MockFs::new(); - fs.add_file("/root/a.md", "a"); - fs.add_file("/root/sub/b.md", "b"); - let files = fs.walk_dir(Path::new("/root")).unwrap(); - assert!(files.contains(&Path::new("/root/a.md").to_path_buf())); - assert!(files.contains(&Path::new("/root/sub/b.md").to_path_buf())); -} - -#[test] -fn read_to_string_mock_error() { - let mut fs = MockFs::new(); - fs.add_file("/faulty.txt", "data"); - fs.read_error = Some(Path::new("/faulty.txt").to_path_buf()); - assert!(fs.read_to_string(Path::new("/faulty.txt")).is_err()); -} diff --git a/tests/fs_real_tests.rs b/tests/fs_real_tests.rs deleted file mode 100644 index 7c51c06..0000000 --- a/tests/fs_real_tests.rs +++ /dev/null @@ -1,43 +0,0 @@ -use librawssg::fs::{FileSystem, real::RealFs}; -use tempfile::tempdir; - -#[test] -fn real_fs_read_write_cycle() { - let dir = tempdir().unwrap(); - let file_path = dir.path().join("test.txt"); - RealFs.write(&file_path, b"hello").unwrap(); - assert!(RealFs.exists(&file_path)); - let content = RealFs.read_to_string(&file_path).unwrap(); - assert_eq!(content, "hello"); -} - -#[test] -fn real_fs_create_and_remove_dir() { - let dir = tempdir().unwrap(); - let sub = dir.path().join("sub"); - RealFs.create_dir_all(&sub).unwrap(); - assert!(RealFs.is_dir(&sub)); - RealFs.remove_dir_all(&sub).unwrap(); - assert!(!RealFs.exists(&sub)); -} - -#[test] -fn real_fs_walk_dir() { - let dir = tempdir().unwrap(); - RealFs.write(&dir.path().join("a.txt"), b"a").unwrap(); - std::fs::create_dir(dir.path().join("sub")).unwrap(); - RealFs.write(&dir.path().join("sub/b.txt"), b"b").unwrap(); - let files = RealFs.walk_dir(dir.path()).unwrap(); - assert!(files.contains(&dir.path().join("a.txt"))); - assert!(files.contains(&dir.path().join("sub/b.txt"))); -} - -#[test] -fn real_fs_copy_file() { - let dir = tempdir().unwrap(); - let src = dir.path().join("src.txt"); - let dst = dir.path().join("dst.txt"); - RealFs.write(&src, b"copy").unwrap(); - RealFs.copy_file(&src, &dst).unwrap(); - assert_eq!(RealFs.read_to_string(&dst).unwrap(), "copy"); -} diff --git a/tests/integration_advanced_tests.rs b/tests/integration_advanced_tests.rs deleted file mode 100644 index 64884c2..0000000 --- a/tests/integration_advanced_tests.rs +++ /dev/null @@ -1,99 +0,0 @@ -mod common; -mod common_mocks; - -#[cfg(all(feature = "tera", feature = "pulldown"))] -mod advanced { - use librawssg::SiteBuilder; - use librawssg::error::RawssgError; - use librawssg::markdown::PulldownMarkdown; - use librawssg::site::TeraRenderer; - use librawssg::types::{ContentTypeDef, RawssgConfig}; - - #[test] - fn builder_missing_markdown_renderer() { - let mut config = RawssgConfig::default(); - config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }); - let result = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(TeraRenderer::new())) - .build(); - match result { - Err(RawssgError::Config(msg)) => assert!(msg.contains("markdown renderer")), - _ => panic!("Expected config error about missing markdown renderer"), - } - } - - #[test] - fn builder_missing_template_renderer() { - let mut config = RawssgConfig::default(); - config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }); - let result = SiteBuilder::new() - .config(config) - .with_markdown_renderer(Box::new(PulldownMarkdown)) - .build(); - match result { - Err(RawssgError::Config(msg)) => assert!(msg.contains("template renderer")), - _ => panic!("Expected config error about missing template renderer"), - } - } - - #[test] - fn rss_enabled_without_context_builder_error() { - let mut config = RawssgConfig::default(); - config.generators.rss.enabled = true; - config.generators.rss.template = "rss.xml".into(); - config.generators.rss.path = "rss.xml".into(); - config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }); - let result = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(TeraRenderer::new())) - .with_markdown_renderer(Box::new(PulldownMarkdown)) - .build(); - match result { - Err(RawssgError::Config(msg)) => assert!(msg.contains("feed context builder")), - _ => panic!("Expected error about missing feed context builder"), - } - } - - #[test] - fn sitemap_enabled_without_context_builder_error() { - let mut config = RawssgConfig::default(); - config.generators.sitemap.enabled = true; - config.generators.sitemap.template = "sitemap.xml".into(); - config.generators.sitemap.path = "sitemap.xml".into(); - config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }); - let result = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(TeraRenderer::new())) - .with_markdown_renderer(Box::new(PulldownMarkdown)) - .build(); - match result { - Err(RawssgError::Config(msg)) => assert!(msg.contains("sitemap context builder")), - _ => panic!("Expected error about missing sitemap context builder"), - } - } -} diff --git a/tests/integration_test.rs b/tests/integration_test.rs deleted file mode 100644 index b9ce158..0000000 --- a/tests/integration_test.rs +++ /dev/null @@ -1,162 +0,0 @@ -mod common; -mod common_mocks; - -#[cfg(all(feature = "tera", feature = "pulldown"))] -mod all_tests { - use crate::common_mocks::context_mocks::{MockFeedContextBuilder, MockSitemapContextBuilder}; - use librawssg::SiteBuilder; - use librawssg::markdown::PulldownMarkdown; - use librawssg::site::TeraRenderer; - use librawssg::types::{ContentTypeDef, RawssgConfig}; - use std::fs; - use tempfile::tempdir; - - #[test] - fn full_site_generation() { - let dir = tempdir().unwrap(); - let content = dir.path().join("content"); - let templates = dir.path().join("templates"); - let output = dir.path().join("dist"); - fs::create_dir(&content).unwrap(); - fs::create_dir(&templates).unwrap(); - - fs::write( - content.join("index.md"), - "---\ntitle: Home\ndesc: Home page\n---\nWelcome!", - ) - .unwrap(); - fs::write( - content.join("about.md"), - "---\ntitle: About\ndesc: About us\n---\nAbout us", - ) - .unwrap(); - - let mut tera = TeraRenderer::new(); - tera.add_raw_template( - "base.html", - "{{ page_title }}{{ page_content }}", - ).unwrap(); - let md = PulldownMarkdown; - - let mut config = RawssgConfig::default(); - config.build.content_dir = content.to_string_lossy().into(); - config.build.output_dir = output.to_string_lossy().into(); - config.build.templates_dir = templates.to_string_lossy().into(); - - config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "**/*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }); - - let site = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .with_feed_context_builder(Box::new(MockFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(MockSitemapContextBuilder)) - .build() - .expect("build failed"); - site.generate().expect("generate failed"); - - assert!(output.join("index.html").exists()); - assert!(output.join("about.html").exists()); - let home_html = fs::read_to_string(output.join("index.html")).unwrap(); - assert!(home_html.contains("Welcome!")); - assert!(home_html.contains("Home")); - } - - #[test] - fn draft_not_in_output() { - let dir = tempdir().unwrap(); - let content = dir.path().join("content"); - let templates = dir.path().join("templates"); - let output = dir.path().join("dist"); - fs::create_dir(&content).unwrap(); - fs::create_dir(&templates).unwrap(); - - fs::write( - content.join("draft.md"), - "---\ntitle: Secret\ndesc: x\ndraft: true\n---\nShh", - ) - .unwrap(); - - let mut tera = TeraRenderer::new(); - tera.add_raw_template("base.html", "{{ page_content }}") - .unwrap(); - let md = PulldownMarkdown; - - let mut config = RawssgConfig::default(); - config.build.content_dir = content.to_string_lossy().into(); - config.build.output_dir = output.to_string_lossy().into(); - config.build.templates_dir = templates.to_string_lossy().into(); - - config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "**/*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }); - - let site = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .with_feed_context_builder(Box::new(MockFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(MockSitemapContextBuilder)) - .build() - .unwrap(); - site.generate().unwrap(); - - assert!(!output.join("draft.html").exists()); - } - - #[test] - fn generate_output_files_basic() { - let dir = tempdir().unwrap(); - let content = dir.path().join("content"); - let templates = dir.path().join("templates"); - let output = dir.path().join("dist"); - fs::create_dir(&content).unwrap(); - fs::create_dir(&templates).unwrap(); - - fs::write( - content.join("about.md"), - "---\ntitle: About\ndesc: x\n---\nAbout content", - ) - .unwrap(); - - let mut tera = TeraRenderer::new(); - tera.add_raw_template("base.html", "{{ page_content }}") - .unwrap(); - let md = PulldownMarkdown; - - let mut config = RawssgConfig::default(); - config.build.content_dir = content.to_string_lossy().into(); - config.build.output_dir = output.to_string_lossy().into(); - config.build.templates_dir = templates.to_string_lossy().into(); - - config.content_types.push(ContentTypeDef { - name: "page".into(), - pattern: "**/*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }); - - let site = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(tera)) - .with_markdown_renderer(Box::new(md)) - .with_feed_context_builder(Box::new(MockFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(MockSitemapContextBuilder)) - .build() - .unwrap(); - site.generate().expect("generate failed"); - - assert!(output.join("about.html").exists()); - } -} diff --git a/tests/markdown_tests.rs b/tests/markdown_tests.rs deleted file mode 100644 index e384e56..0000000 --- a/tests/markdown_tests.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[cfg(feature = "pulldown")] -mod markdown_tests { - use librawssg::markdown::{MarkdownRenderer, PulldownMarkdown}; - - #[test] - fn basic_markdown() { - let html = PulldownMarkdown.render("# Hello\nWorld"); - assert!(html.contains("

")); - assert!(html.contains("World")); - } - - #[test] - fn tables_rendered() { - let input = "| A | B |\n|---|---|\n| 1 | 2 |"; - let html = PulldownMarkdown.render(input); - assert!(html.contains("")); - } - - #[test] - fn strikethrough() { - let html = PulldownMarkdown.render("~~deleted~~"); - assert!(html.contains("")); - } - - #[test] - fn task_list() { - let html = PulldownMarkdown.render("- [ ] task\n- [x] done"); - assert!(html.contains("checkbox")); - } - - #[test] - fn empty_input() { - assert_eq!(PulldownMarkdown.render(""), ""); - } -} diff --git a/tests/property_tests.proptest-regressions b/tests/property_tests.proptest-regressions deleted file mode 100644 index 07ff2da..0000000 --- a/tests/property_tests.proptest-regressions +++ /dev/null @@ -1,7 +0,0 @@ -# Seeds for failure cases proptest has generated in the past. It is -# automatically read and these particular cases re-run before any -# novel cases are generated. -# -# It is recommended to check this file in to source control so that -# everyone who runs the test benefits from these saved cases. -cc 3af87c54bbb988799deb6c5d4d4a7821893da1be3436e1b14b6ecffb0b13c787 # shrinks to s = "𝔖" diff --git a/tests/property_tests.rs b/tests/property_tests.rs deleted file mode 100644 index f25c356..0000000 --- a/tests/property_tests.rs +++ /dev/null @@ -1,32 +0,0 @@ -use librawssg::util::{match_pattern, relative_prefix, slugify}; -use proptest::prelude::*; - -proptest! { - #[test] - fn slugify_does_not_panic(s in "\\PC*") { - let _ = slugify(&s); - } - - #[test] - fn relative_prefix_never_empty_for_any_depth(depth in 0usize..100) { - let prefix = relative_prefix(depth); - assert!(!prefix.is_empty()); - assert!(prefix.ends_with('/')); - assert_eq!(prefix.matches("../").count(), depth); - } - - #[test] - fn match_pattern_exact_self(path in "[a-zA-Z0-9_/]+\\.md") { - assert!(match_pattern(&path, std::path::Path::new(&path))); - } - - #[test] - fn match_pattern_double_star_matches_everything(path in "[a-zA-Z0-9_/]+\\.md") { - assert!(match_pattern("**", std::path::Path::new(&path))); - } - - #[test] - fn match_pattern_wildcard_segment(path in "blog/[a-z]+\\.md") { - assert!(match_pattern("blog/*.md", std::path::Path::new(&path))); - } -} diff --git a/tests/serve_tests.rs b/tests/serve_tests.rs deleted file mode 100644 index 3567fdb..0000000 --- a/tests/serve_tests.rs +++ /dev/null @@ -1,74 +0,0 @@ -#![cfg(feature = "serve")] -use librawssg::fs::{FileSystem, real::RealFs}; -use librawssg::serve::start_dev_server; -use std::io::{Read, Write}; -use std::net::{TcpListener, TcpStream}; -use std::thread; -use std::time::Duration; -use tempfile::tempdir; - -fn http_get(port: u16, path: &str) -> String { - let mut stream = TcpStream::connect(("127.0.0.1", port)).expect("cannot connect"); - let request = format!("GET {} HTTP/1.0\r\nHost: localhost\r\n\r\n", path); - stream.write_all(request.as_bytes()).unwrap(); - stream.flush().unwrap(); - let mut response = String::new(); - stream.read_to_string(&mut response).unwrap(); - response -} - -fn find_available_port() -> u16 { - TcpListener::bind("127.0.0.1:0") - .expect("failed to bind") - .local_addr() - .unwrap() - .port() -} - -#[test] -fn server_serves_index_html() { - let dir = tempdir().unwrap(); - let index_path = dir.path().join("index.html"); - RealFs.write(&index_path, b"

Hello

").unwrap(); - - let port = find_available_port(); - let dist = dir.path().to_path_buf(); - let server_handle = thread::spawn(move || { - let _ = start_dev_server(&dist, port); - }); - - thread::sleep(Duration::from_millis(500)); - - let response = http_get(port, "/"); - assert!(response.contains("200 OK") || response.contains("HTTP/1.0 200")); - assert!(response.contains("

Hello

")); - - drop(server_handle); -} - -#[test] -fn server_returns_500_for_missing_file() { - let dir = tempdir().unwrap(); - let port = find_available_port(); - let dist = dir.path().to_path_buf(); - let server_handle = thread::spawn(move || { - let _ = start_dev_server(&dist, port); - }); - - let mut response = String::new(); - for _ in 0..10 { - thread::sleep(Duration::from_millis(100)); - if let Ok(res) = std::panic::catch_unwind(|| http_get(port, "/nope.html")) { - response = res; - break; - } - } - - assert!( - response.contains("500"), - "Expected 500 status, got: {}", - response - ); - - drop(server_handle); -} diff --git a/tests/site_builder_tests.rs b/tests/site_builder_tests.rs deleted file mode 100644 index 6753f62..0000000 --- a/tests/site_builder_tests.rs +++ /dev/null @@ -1,148 +0,0 @@ -mod common; -mod common_mocks; - -#[cfg(feature = "tera")] -mod tera_tests { - use crate::common::{MockMarkdownRenderer, MockTemplateRenderer}; - use crate::common_mocks::context_mocks::{MockFeedContextBuilder, MockSitemapContextBuilder}; - use librawssg::SiteBuilder; - use librawssg::types::{ContentTypeDef, RawssgConfig}; - use std::fs; - use tempfile::tempdir; - - fn make_config() -> RawssgConfig { - let mut cfg = RawssgConfig::default(); - cfg.content_types.clear(); - cfg.build.content_dir = "content".into(); - cfg.build.output_dir = "dist".into(); - cfg.build.templates_dir = "templates".into(); - cfg.build.static_dir = "static".into(); - cfg.content_types.push(ContentTypeDef { - name: "blog".into(), - pattern: "blog/*".into(), - template: "post.html".into(), - list_template: Some("blog_list.html".into()), - list_enabled: true, - }); - cfg - } - - #[test] - fn build_simple_site() { - let dir = tempdir().unwrap(); - let content = dir.path().join("content"); - let templates = dir.path().join("templates"); - let output = dir.path().join("dist"); - fs::create_dir(&content).unwrap(); - fs::create_dir(&templates).unwrap(); - - fs::write( - content.join("index.md"), - "---\ntitle: Home\ndesc: Home page\n---\nHello", - ) - .unwrap(); - fs::write(templates.join("base.html"), "{{ page_title }}").unwrap(); - - let mut config = make_config(); - config.build.content_dir = content.to_string_lossy().into(); - config.build.output_dir = output.to_string_lossy().into(); - config.build.templates_dir = templates.to_string_lossy().into(); - - let site = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(MockTemplateRenderer::new())) - .with_markdown_renderer(Box::new(MockMarkdownRenderer::identity())) - .with_feed_context_builder(Box::new(MockFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(MockSitemapContextBuilder)) - .build() - .expect("build should succeed"); - - assert_eq!(site.pages().len(), 1); - assert_eq!(site.pages()[0].url, "index.html"); - } - - #[test] - fn draft_skipped_during_build() { - let dir = tempdir().unwrap(); - let content = dir.path().join("content"); - let templates = dir.path().join("templates"); - fs::create_dir(&content).unwrap(); - fs::create_dir(&templates).unwrap(); - - fs::write( - content.join("draft.md"), - "---\ntitle: Draft\ndesc: x\ndraft: true\n---\n...", - ) - .unwrap(); - fs::write(templates.join("base.html"), "base").unwrap(); - - let mut config = make_config(); - config.build.content_dir = content.to_string_lossy().into(); - config.build.output_dir = dir.path().join("dist").to_string_lossy().into(); - config.build.templates_dir = templates.to_string_lossy().into(); - - let site = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(MockTemplateRenderer::new())) - .with_markdown_renderer(Box::new(MockMarkdownRenderer::identity())) - .with_feed_context_builder(Box::new(MockFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(MockSitemapContextBuilder)) - .build() - .unwrap(); - assert!(site.pages().is_empty()); - } - - #[test] - fn blog_list_page_generated() { - let dir = tempdir().unwrap(); - let content = dir.path().join("content"); - let templates = dir.path().join("templates"); - fs::create_dir(&content).unwrap(); - fs::create_dir(&templates).unwrap(); - - fs::create_dir(content.join("blog")).unwrap(); - fs::write( - content.join("blog/post1.md"), - "---\ntitle: P1\ndesc: x\ndate: 2025-01-01\n---\nOne", - ) - .unwrap(); - fs::write( - content.join("blog/post2.md"), - "---\ntitle: P2\ndesc: x\ndate: 2025-01-02\n---\nTwo", - ) - .unwrap(); - fs::write(templates.join("base.html"), "{{ page_title }}").unwrap(); - fs::write(templates.join("post.html"), "post").unwrap(); - fs::write(templates.join("blog_list.html"), "list").unwrap(); - - let mut config = make_config(); - config.build.content_dir = content.to_string_lossy().into(); - config.build.output_dir = dir.path().join("dist").to_string_lossy().into(); - config.build.templates_dir = templates.to_string_lossy().into(); - - let site = SiteBuilder::new() - .config(config) - .with_template_renderer(Box::new(MockTemplateRenderer::new())) - .with_markdown_renderer(Box::new(MockMarkdownRenderer::identity())) - .with_feed_context_builder(Box::new(MockFeedContextBuilder)) - .with_sitemap_context_builder(Box::new(MockSitemapContextBuilder)) - .build() - .unwrap(); - - assert_eq!(site.pages().len(), 3); - let list = site.pages().iter().find(|p| p.is_list).unwrap(); - assert_eq!(list.url, "blog/index.html"); - assert_eq!(list.list_items.as_ref().unwrap().len(), 2); - } -} - -#[test] -fn builder_new_works() { - let _builder = librawssg::SiteBuilder::new(); -} - -#[test] -fn builder_load_config_file_not_found() { - let result = librawssg::SiteBuilder::new().load_config("nonexistent.yaml"); - assert!(result.is_err()); -} diff --git a/tests/site_page_tests.rs b/tests/site_page_tests.rs deleted file mode 100644 index 6e07e4f..0000000 --- a/tests/site_page_tests.rs +++ /dev/null @@ -1,107 +0,0 @@ -mod common; -use common::{MockFs, MockMarkdownRenderer}; -use librawssg::error::RawssgError; -use librawssg::site::page::build_page_context; -use std::path::Path; - -#[test] -fn successful_page_context() { - let mut fs = MockFs::new(); - fs.add_file( - "/content/about.md", - "---\ntitle: About\ndesc: About page\n---\nHello", - ); - let renderer = MockMarkdownRenderer::identity(); - let ctx = build_page_context( - &fs, - &renderer, - Path::new("/content/about.md"), - Path::new("/content"), - ) - .unwrap() - .expect("should return some"); - assert_eq!(ctx.frontmatter.title, "About"); - assert_eq!(ctx.content_html, "Hello"); - assert_eq!(ctx.url, "about.html"); - assert_eq!(ctx.depth, 0); - assert!(ctx.pub_date.is_none()); -} - -#[test] -fn draft_page_returns_none() { - let mut fs = MockFs::new(); - fs.add_file( - "/content/draft.md", - "---\ntitle: Draft\ndesc: x\ndraft: true\n---\nContent", - ); - let renderer = MockMarkdownRenderer::identity(); - let result = build_page_context( - &fs, - &renderer, - Path::new("/content/draft.md"), - Path::new("/content"), - ) - .unwrap(); - assert!(result.is_none()); -} - -#[test] -fn date_formatting() { - let mut fs = MockFs::new(); - fs.add_file( - "/content/post.md", - "---\ntitle: Post\ndesc: x\ndate: 2025-01-01\n---\nBody", - ); - let renderer = MockMarkdownRenderer::identity(); - let ctx = build_page_context( - &fs, - &renderer, - Path::new("/content/post.md"), - Path::new("/content"), - ) - .unwrap() - .unwrap(); - assert_eq!( - ctx.pub_date, - Some("Wed, 01 Jan 2025 00:00:00 +0000".to_string()) - ); -} - -#[test] -fn nested_depth_calculation() { - let mut fs = MockFs::new(); - fs.add_file( - "/content/blog/post.md", - "---\ntitle: Nested\ndesc: x\n---\n", - ); - let renderer = MockMarkdownRenderer::identity(); - let ctx = build_page_context( - &fs, - &renderer, - Path::new("/content/blog/post.md"), - Path::new("/content"), - ) - .unwrap() - .unwrap(); - assert_eq!(ctx.url, "blog/post.html"); - assert_eq!(ctx.depth, 1); -} - -#[test] -fn read_error_propagates() { - let mut fs = MockFs::new(); - fs.add_file("/content/fault.md", "anything"); - fs.read_error = Some(Path::new("/content/fault.md").to_path_buf()); - let renderer = MockMarkdownRenderer::identity(); - let err = build_page_context( - &fs, - &renderer, - Path::new("/content/fault.md"), - Path::new("/content"), - ) - .unwrap_err(); - match err { - RawssgError::Io(_) => {} - _ => panic!("expected Io error"), - } -} diff --git a/tests/util_property_tests.rs b/tests/util_property_tests.rs deleted file mode 100644 index 928d627..0000000 --- a/tests/util_property_tests.rs +++ /dev/null @@ -1,19 +0,0 @@ -use librawssg::util::slugify; -use proptest::prelude::*; - -proptest! { - #[test] - fn slugify_no_consecutive_dashes(s in "\\PC*") { - let result = slugify(&s); - assert!(!result.contains("--")); - } - - #[test] - fn slugify_no_leading_or_trailing_dash(s in "\\PC*") { - let result = slugify(&s); - if !result.is_empty() { - assert!(!result.starts_with('-')); - assert!(!result.ends_with('-')); - } - } -} diff --git a/tests/util_tests.rs b/tests/util_tests.rs deleted file mode 100644 index 7e3d1c1..0000000 --- a/tests/util_tests.rs +++ /dev/null @@ -1,81 +0,0 @@ -use librawssg::error::RawssgError; -use librawssg::fs::real::RealFs; -use librawssg::util::{match_pattern, relative_prefix, safe_path, slugify}; -use std::path::Path; -use tempfile::tempdir; - -#[test] -fn safe_path_inside_base() { - let dir = tempdir().unwrap(); - let base = dir.path().canonicalize().unwrap(); - let candidate = base.join("file.txt"); - std::fs::write(&candidate, "").unwrap(); - let result = safe_path(&RealFs, &base, &candidate).unwrap(); - assert_eq!(result, candidate.canonicalize().unwrap()); -} - -#[test] -fn safe_path_outside_base() { - let dir = tempdir().unwrap(); - let base = dir.path().join("sub"); - std::fs::create_dir(&base).unwrap(); - let base = base.canonicalize().unwrap(); - - let outside = dir.path().join("outside.txt"); - std::fs::write(&outside, "test").unwrap(); - let outside = outside.canonicalize().unwrap(); - - match safe_path(&RealFs, &base, &outside) { - Err(RawssgError::PathTraversal(_)) => {} - _ => panic!("expected PathTraversal error"), - } -} - -#[test] -fn slugify_simple() { - assert_eq!(slugify("Hello World"), "hello-world"); -} - -#[test] -fn slugify_special_chars() { - assert_eq!(slugify("Rust & SSG!"), "rust-ssg"); -} - -#[test] -fn slugify_multiple_dashes() { - assert_eq!(slugify("A--B"), "a-b"); -} - -#[test] -fn relative_prefix_depths() { - assert_eq!(relative_prefix(0), "./"); - assert_eq!(relative_prefix(1), "../"); - assert_eq!(relative_prefix(3), "../../../"); -} - -#[test] -fn match_pattern_exact() { - assert!(match_pattern("blog/post.md", Path::new("blog/post.md"))); -} - -#[test] -fn match_pattern_wildcard_single() { - assert!(match_pattern("blog/*.md", Path::new("blog/hello.md"))); - assert!(!match_pattern("blog/*.md", Path::new("blog/sub/hello.md"))); -} - -#[test] -fn match_pattern_double_wildcard() { - assert!(match_pattern("blog/**", Path::new("blog/a/b/c.md"))); - assert!(match_pattern("blog/**", Path::new("blog/file.md"))); -} - -#[test] -fn match_pattern_too_long() { - assert!(!match_pattern("a/b/c", Path::new("a/b"))); -} - -#[test] -fn match_pattern_empty_pattern() { - assert!(!match_pattern("", Path::new("anything"))); -} diff --git a/tests/watcher_tests.rs b/tests/watcher_tests.rs deleted file mode 100644 index 0dfc83b..0000000 --- a/tests/watcher_tests.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![cfg(feature = "serve")] -use librawssg::serve::watch_dirs; -use std::sync::mpsc; -use std::time::Duration; -use tempfile::tempdir; - -#[test] -fn watcher_triggers_on_file_create() { - let dir = tempdir().unwrap(); - let (tx, rx) = mpsc::channel(); - let on_change = move || { - tx.send(()).ok(); - }; - let _watcher = watch_dirs(&[dir.path().to_path_buf()], on_change).expect("watcher start"); - - std::fs::write(dir.path().join("new.md"), b"content").unwrap(); - - let received = rx.recv_timeout(Duration::from_secs(3)); - assert!(received.is_ok(), "watcher should have triggered"); -} From 19f1bdc0a1eef51cf8eff0156fd128e421ac55a8 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 00:40:59 +0700 Subject: [PATCH 02/20] refactor: split into workspace crates (#25) * chore: update Cargo.lock for workspace split Remove dependency entries for the old single-crate project and retain only entries for the three new workspace crates. This reflects the transition to a workspace layout. * refactor: convert to Cargo workspace with three crates Replace the single `librawssg` package with a workspace containing `librawssg_compiler`, `librawssg_handler`, and `librawssg_templates`. Add strict workspace-level lints for clippy and rustc. Remove all old dependencies and features. * refactor: remove src/config/loader.rs Delete the old YAML config loader implementation. Configuration handling will be reimplemented in the new workspace crates. * refactor: remove src/config/mod.rs Delete the old configuration module. Configuration functionality will be split across the new workspace crates. * refactor: remove src/error.rs Delete the old error type definitions. Error types will be redefined in the new workspace crates. * refactor: remove src/frontmatter.rs Delete the frontmatter parser. This logic will be moved to the appropriate new crate. * refactor: remove src/fs/mod.rs Delete the filesystem abstraction trait. Filesystem operations will be handled by the new crates. * refactor: remove src/fs/real.rs Delete the real filesystem implementation. It will be replaced by functionality in the new crates. * refactor: remove src/lib.rs Delete the old crate root. The library is now a workspace of multiple crates. * refactor: remove src/markdown.rs Delete the Markdown rendering trait and pulldown implementation. Markdown handling will be moved to the compiler crate. * refactor: remove src/serve/mod.rs Delete the development server module. Serving functionality will be reimplemented in the handler crate. * refactor: remove src/serve/watcher.rs Delete the file watcher module. Watch functionality will be part of the handler crate. * refactor: remove src/site/builders/mod.rs Delete the builders module declaration. Site building logic will be relocated. * refactor: remove src/site/builders/site.rs Delete the Site builder implementation. This core logic will be moved to the compiler crate. * refactor: remove src/site/builders/site_builder.rs Delete the SiteBuilder implementation. It will be reimplemented in the compiler crate. * refactor: remove src/site/context.rs Delete the context builder traits and Tera implementations. Context handling moves to the templates crate. * refactor: remove src/site/feed.rs Delete the RSS feed generation module. Feed generation will be part of the compiler crate. * refactor: remove src/site/mod.rs Delete the main site module that defined traits and handlers. These will be split across new crates. * refactor: remove src/site/page.rs Delete the page building logic. Page processing will be implemented in the compiler crate. * refactor: remove src/site/sitemap.rs Delete the sitemap generation module. This functionality moves to the compiler crate. * refactor: remove src/types.rs Delete the old configuration and page types. New type definitions will live in the appropriate workspace crates. * refactor: remove src/util.rs Delete utility functions including safe_path, slugify, and pattern matching. These will be reimplemented in the new crates. * feat: add librawssg_compiler crate manifest Create Cargo.toml for the new compiler crate, using workspace lints and no dependencies. * feat: add librawssg_compiler placeholder lib Add a placeholder library file with a simple add function and test to bootstrap the compiler crate. * feat: add librawssg_handler crate manifest Create Cargo.toml for the new handler crate, using workspace lints and no dependencies. * feat: add librawssg_handler placeholder lib Add a placeholder library file with a simple add function and test to bootstrap the handler crate. * feat: add librawssg_templates crate manifest Create Cargo.toml for the new templates crate, using workspace lints and no dependencies. * feat: add librawssg_templates placeholder lib Add a placeholder library file with a simple add function and test to bootstrap the templates crate. --- Cargo.lock | 1187 +---------------------------- Cargo.toml | 138 +++- librawssg_compiler/Cargo.toml | 9 + librawssg_compiler/src/lib.rs | 14 + librawssg_handler/Cargo.toml | 9 + librawssg_handler/src/lib.rs | 14 + librawssg_templates/Cargo.toml | 9 + librawssg_templates/src/lib.rs | 14 + src/config/loader.rs | 32 - src/config/mod.rs | 14 - src/error.rs | 55 -- src/frontmatter.rs | 44 -- src/fs/mod.rs | 20 - src/fs/real.rs | 84 -- src/lib.rs | 17 - src/markdown.rs | 21 - src/serve/mod.rs | 98 --- src/serve/watcher.rs | 41 - src/site/builders/mod.rs | 2 - src/site/builders/site.rs | 314 -------- src/site/builders/site_builder.rs | 242 ------ src/site/context.rs | 59 -- src/site/feed.rs | 21 - src/site/mod.rs | 116 --- src/site/page.rs | 58 -- src/site/sitemap.rs | 21 - src/types.rs | 246 ------ src/util.rs | 179 ----- 28 files changed, 173 insertions(+), 2905 deletions(-) create mode 100644 librawssg_compiler/Cargo.toml create mode 100644 librawssg_compiler/src/lib.rs create mode 100644 librawssg_handler/Cargo.toml create mode 100644 librawssg_handler/src/lib.rs create mode 100644 librawssg_templates/Cargo.toml create mode 100644 librawssg_templates/src/lib.rs delete mode 100644 src/config/loader.rs delete mode 100644 src/config/mod.rs delete mode 100644 src/error.rs delete mode 100644 src/frontmatter.rs delete mode 100644 src/fs/mod.rs delete mode 100644 src/fs/real.rs delete mode 100644 src/lib.rs delete mode 100644 src/markdown.rs delete mode 100644 src/serve/mod.rs delete mode 100644 src/serve/watcher.rs delete mode 100644 src/site/builders/mod.rs delete mode 100644 src/site/builders/site.rs delete mode 100644 src/site/builders/site_builder.rs delete mode 100644 src/site/context.rs delete mode 100644 src/site/feed.rs delete mode 100644 src/site/mod.rs delete mode 100644 src/site/page.rs delete mode 100644 src/site/sitemap.rs delete mode 100644 src/types.rs delete mode 100644 src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 75d8594..cb330d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,1188 +3,13 @@ version = 4 [[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] +name = "librawssg_compiler" +version = "0.1.0" [[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +name = "librawssg_handler" +version = "0.1.0" [[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.2.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width 0.2.2", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inotify" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - -[[package]] -name = "is_ci" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "kqueue" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "librawssg" -version = "0.5.0" -dependencies = [ - "chrono", - "glob", - "miette", - "notify", - "proptest", - "pulldown-cmark", - "serde", - "serde_yaml", - "tempfile", - "tera", - "thiserror", - "tiny_http", - "tracing", - "walkdir", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "backtrace", - "backtrace-ext", - "cfg-if", - "miette-derive", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "owo-colors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags", - "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "pulldown-cmark" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" -dependencies = [ - "bitflags", - "getopts", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rustc-demangle" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "supports-color" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" -dependencies = [ - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" - -[[package]] -name = "supports-unicode" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tera" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511f07fd91a70e92efbe4793d111aaa9035f8474dd157aaa1e31e7c27f5051da" -dependencies = [ - "serde", -] - -[[package]] -name = "terminal_size" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" -dependencies = [ - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "unicode-linebreak", - "unicode-width 0.2.2", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tiny_http" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" -dependencies = [ - "ascii", - "chunked_transfer", - "httpdate", - "log", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "zerocopy" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +name = "librawssg_templates" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 722a3df..bb365b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,42 +1,100 @@ -[package] -name = "librawssg" -version = "0.5.0" -edition = "2024" -description = "Engine-agnostic, safety-first kernel for building static site generators in Rust" -license = "MIT" -repository = "https://github.com/mroczect/librawssg" -homepage = "https://github.com/mroczect/librawssg" -documentation = "https://docs.rs/librawssg" -readme = "README.md" -keywords = ["static-site", "ssg", "libary", "kernel", "rawssg"] -categories = ["web-programming", "template-engine", "development-tools"] +[workspace] +resolver = "3" +members = ["librawssg_compiler","librawssg_handler", "librawssg_templates"] -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_yaml = "0.9" -thiserror = "2" -miette = { version = "7", features = ["fancy"] } -tracing = "0.1" -walkdir = "2" -chrono = { version = "0.4", features = ["serde"] } -glob = "0.3" +[workspace.lints.clippy] +all = { level = "deny", priority = -1 } +alloc_instead_of_core = "deny" +allow_attributes = "allow" +allow_attributes_without_reason = "allow" +arithmetic_side_effects = "deny" +cargo = { level = "deny", priority = -1 } +complexity = { level = "deny", priority = -1 } +correctness = { level = "deny", priority = -1 } +doc_lazy_continuation = "allow" +doc_markdown = "allow" +empty_docs = "allow" +expect_used = "deny" +implicit_hasher = "allow" +indexing_slicing = "deny" +map_err_ignore = "deny" +match_same_arms = "allow" +missing_docs_in_private_items = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +missing_safety_doc = "allow" +module_name_repetitions = "allow" +needless_doctest_main = "allow" +needless_return = "allow" +nursery = { level = "deny", priority = -1 } +panic = "deny" +pedantic = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +std_instead_of_alloc = "deny" +std_instead_of_core = "deny" +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +uninlined_format_args = "allow" +unwrap_used = "deny" +wildcard_enum_match_arm = "deny" -tera = { version = "2", optional = true } -pulldown-cmark = { version = "0.13", optional = true } - -# Serve feature -tiny_http = { version = "0.12", optional = true } -notify = { version = "8", optional = true } - -[features] -default = [] -tera = ["dep:tera"] -pulldown = ["dep:pulldown-cmark"] -serve = ["dep:tiny_http", "dep:notify"] - -[dev-dependencies] -pulldown-cmark = "0.13" -tempfile = "3.27.0" -tera = "2" - -proptest = "1.11.0" \ No newline at end of file +[workspace.lints.rust] +deprecated = "deny" +elided_lifetimes_in_paths = "deny" +explicit_outlives_requirements = "deny" +future_incompatible = { level = "deny", priority = -1 } +invalid_reference_casting = "deny" +macro_use_extern_crate = "deny" +missing_copy_implementations = "deny" +missing_debug_implementations = "deny" +missing_docs = "allow" +no_mangle_generic_items = "deny" +non_ascii_idents = "deny" +non_camel_case_types = "deny" +non_snake_case = "deny" +non_upper_case_globals = "deny" +noop_method_call = "deny" +overlapping_range_endpoints = "deny" +private_bounds = "deny" +private_interfaces = "deny" +redundant_lifetimes = "deny" +renamed_and_removed_lints = "deny" +rust_2018_idioms = { level = "deny", priority = -1 } +rust_2021_compatibility = { level = "deny", priority = -1 } +rust_2024_compatibility = { level = "deny", priority = -1 } +single_use_lifetimes = "deny" +trivial_bounds = "deny" +trivial_casts = "deny" +trivial_numeric_casts = "deny" +unexpected_cfgs = "deny" +uninhabited_static = "deny" +unit_bindings = "deny" +unknown_lints = "deny" +unnameable_types = "deny" +unreachable_code = "deny" +unreachable_patterns = "deny" +unreachable_pub = "deny" +unsafe_code = "forbid" +unsafe_op_in_unsafe_fn = "deny" +unused = { level = "deny", priority = -1 } +unused_allocation = "deny" +unused_assignments = "deny" +unused_braces = "deny" +unused_comparisons = "deny" +unused_crate_dependencies = "deny" +unused_doc_comments = "allow" +unused_extern_crates = "deny" +unused_features = "deny" +unused_imports = "deny" +unused_labels = "deny" +unused_lifetimes = "deny" +unused_macro_rules = "deny" +unused_macros = "deny" +unused_must_use = "deny" +unused_mut = "deny" +unused_parens = "deny" +unused_qualifications = "deny" +unused_results = "deny" +unused_unsafe = "deny" +unused_variables = "deny" +warnings = "deny" diff --git a/librawssg_compiler/Cargo.toml b/librawssg_compiler/Cargo.toml new file mode 100644 index 0000000..d31f713 --- /dev/null +++ b/librawssg_compiler/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "librawssg_compiler" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lints] +workspace = true diff --git a/librawssg_compiler/src/lib.rs b/librawssg_compiler/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/librawssg_compiler/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/librawssg_handler/Cargo.toml b/librawssg_handler/Cargo.toml new file mode 100644 index 0000000..ca51fb3 --- /dev/null +++ b/librawssg_handler/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "librawssg_handler" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lints] +workspace = true diff --git a/librawssg_handler/src/lib.rs b/librawssg_handler/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/librawssg_handler/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/librawssg_templates/Cargo.toml b/librawssg_templates/Cargo.toml new file mode 100644 index 0000000..6900f48 --- /dev/null +++ b/librawssg_templates/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "librawssg_templates" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lints] +workspace = true diff --git a/librawssg_templates/src/lib.rs b/librawssg_templates/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/librawssg_templates/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/src/config/loader.rs b/src/config/loader.rs deleted file mode 100644 index 86dc6e3..0000000 --- a/src/config/loader.rs +++ /dev/null @@ -1,32 +0,0 @@ -use super::ConfigLoader; -use crate::error::RawssgError; -use crate::types::RawssgConfig; -use std::path::Path; - -pub struct YamlConfigLoader + Send + Sync> { - path: P, -} - -impl + Send + Sync> YamlConfigLoader

{ - pub fn new(path: P) -> Self { - Self { path } - } -} - -impl + Send + Sync> ConfigLoader for YamlConfigLoader

{ - #[tracing::instrument(skip(self))] - fn load(&self) -> Result { - let content = std::fs::read_to_string(self.path.as_ref()) - .map_err(|e| RawssgError::Config(format!("cannot read config: {}", e)))?; - serde_yaml::from_str(&content) - .map_err(|e| RawssgError::Config(format!("invalid config YAML: {}", e))) - } -} - -pub struct DefaultConfig; - -impl ConfigLoader for DefaultConfig { - fn load(&self) -> Result { - Ok(RawssgConfig::default()) - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index 95728ae..0000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod loader; - -use crate::error::RawssgError; -use crate::types::RawssgConfig; - -pub trait ConfigLoader: Send + Sync { - fn load(&self) -> Result; - fn load_or_default(&self) -> RawssgConfig { - self.load().unwrap_or_else(|e| { - tracing::error!("Failed to load config, using defaults: {}", e); - RawssgConfig::default() - }) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index dd08fb0..0000000 --- a/src/error.rs +++ /dev/null @@ -1,55 +0,0 @@ -use miette::Diagnostic; -use thiserror::Error; - -#[derive(Error, Debug, Diagnostic)] -pub enum RawssgError { - #[error("I/O error")] - #[diagnostic(code(rawssg::io))] - Io(#[from] std::io::Error), - - #[error("Configuration error: {0}")] - #[diagnostic(code(rawssg::config))] - Config(String), - - #[error("Failed to parse frontmatter in {path}")] - #[diagnostic( - code(rawssg::frontmatter), - help("Check the YAML frontmatter syntax and ensure the file starts with '---'") - )] - Frontmatter { - path: std::path::PathBuf, - #[source] - source: Box, - }, - - #[error("Template rendering error: {0}")] - #[diagnostic(code(rawssg::template))] - Template(String), - - #[error("Path traversal attempt detected: {0}")] - #[diagnostic( - code(rawssg::path_traversal), - help("All file paths must stay within the project directory") - )] - PathTraversal(String), - - #[error("Missing configuration key: {0}")] - #[diagnostic(code(rawssg::missing_config))] - MissingConfig(String), - - #[error("Markdown processing error: {0}")] - #[diagnostic(code(rawssg::markdown))] - Markdown(String), - - #[error("Site generation error: {0}")] - #[diagnostic(code(rawssg::site))] - SiteGeneration(String), - - #[error("Resource not found: {0}")] - #[diagnostic(code(rawssg::not_found))] - NotFound(String), - - #[error("Internal error: {0}")] - #[diagnostic(code(rawssg::internal))] - Internal(String), -} diff --git a/src/frontmatter.rs b/src/frontmatter.rs deleted file mode 100644 index f4129ef..0000000 --- a/src/frontmatter.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::error::RawssgError; -use crate::markdown::MarkdownRenderer; -use crate::types::PageFrontMatter; -use std::path::Path; - -#[tracing::instrument(skip(raw, renderer))] -pub fn parse_frontmatter_and_render( - raw: &str, - path: &Path, - renderer: &dyn MarkdownRenderer, -) -> Result<(PageFrontMatter, String), RawssgError> { - let trimmed = raw.trim_start(); - if !trimmed.starts_with("---") { - return Err(RawssgError::Frontmatter { - path: path.to_path_buf(), - source: "missing opening '---'".into(), - }); - } - let without_first = trimmed.trim_start_matches("---").trim_start(); - let end = without_first - .find("\n---") - .or_else(|| without_first.find("\r\n---")); - let end = match end { - Some(pos) => pos, - None => { - return Err(RawssgError::Frontmatter { - path: path.to_path_buf(), - source: "missing closing '---'".into(), - }); - } - }; - let yaml_str = &without_first[..end]; - let markdown_str = without_first[end..] - .trim_start_matches("\n---") - .trim_start_matches("\r\n---") - .trim(); - let fm: PageFrontMatter = - serde_yaml::from_str(yaml_str).map_err(|e| RawssgError::Frontmatter { - path: path.to_path_buf(), - source: Box::new(e), - })?; - let html = renderer.render(markdown_str); - Ok((fm, html)) -} diff --git a/src/fs/mod.rs b/src/fs/mod.rs deleted file mode 100644 index bad9b8a..0000000 --- a/src/fs/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::io; -use std::path::{Path, PathBuf}; - -pub trait FileSystem: Send + Sync { - fn read_to_string(&self, path: &Path) -> io::Result; - fn read_bytes(&self, path: &Path) -> io::Result>; - fn write(&self, path: &Path, content: &[u8]) -> io::Result<()>; - fn create_dir_all(&self, path: &Path) -> io::Result<()>; - fn remove_dir_all(&self, path: &Path) -> io::Result<()>; - fn exists(&self, path: &Path) -> bool; - fn is_dir(&self, path: &Path) -> bool; - fn is_file(&self, path: &Path) -> bool; - fn read_dir(&self, path: &Path) -> io::Result>; - fn copy_file(&self, from: &Path, to: &Path) -> io::Result; - fn walk_dir(&self, root: &Path) -> io::Result>; - fn canonicalize(&self, path: &Path) -> io::Result; - fn rename(&self, from: &Path, to: &Path) -> io::Result<()>; -} - -pub mod real; diff --git a/src/fs/real.rs b/src/fs/real.rs deleted file mode 100644 index 120798c..0000000 --- a/src/fs/real.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::FileSystem; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; -use walkdir::WalkDir; - -pub struct RealFs; - -impl FileSystem for RealFs { - #[tracing::instrument(skip(self))] - fn read_to_string(&self, path: &Path) -> io::Result { - fs::read_to_string(path) - } - - #[tracing::instrument(skip(self))] - fn read_bytes(&self, path: &Path) -> io::Result> { - fs::read(path) - } - - #[tracing::instrument(skip(self, content))] - fn write(&self, path: &Path, content: &[u8]) -> io::Result<()> { - if let Some(parent) = path.parent() { - self.create_dir_all(parent)?; - } - fs::write(path, content) - } - - #[tracing::instrument(skip(self))] - fn create_dir_all(&self, path: &Path) -> io::Result<()> { - fs::create_dir_all(path) - } - - #[tracing::instrument(skip(self))] - fn remove_dir_all(&self, path: &Path) -> io::Result<()> { - fs::remove_dir_all(path) - } - - #[tracing::instrument(skip(self))] - fn exists(&self, path: &Path) -> bool { - path.exists() - } - - #[tracing::instrument(skip(self))] - fn is_dir(&self, path: &Path) -> bool { - path.is_dir() - } - - #[tracing::instrument(skip(self))] - fn is_file(&self, path: &Path) -> bool { - path.is_file() - } - - #[tracing::instrument(skip(self))] - fn read_dir(&self, path: &Path) -> io::Result> { - let mut entries = Vec::new(); - for entry in fs::read_dir(path)? { - entries.push(entry?.path()); - } - Ok(entries) - } - - #[tracing::instrument(skip(self))] - fn copy_file(&self, from: &Path, to: &Path) -> io::Result { - fs::copy(from, to) - } - - #[tracing::instrument(skip(self))] - fn walk_dir(&self, root: &Path) -> io::Result> { - let mut files = Vec::new(); - for entry in WalkDir::new(root) { - let entry = entry?; - if entry.file_type().is_file() { - files.push(entry.into_path()); - } - } - Ok(files) - } - fn canonicalize(&self, path: &Path) -> io::Result { - path.canonicalize() - } - fn rename(&self, from: &Path, to: &Path) -> io::Result<()> { - std::fs::rename(from, to) - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 13849e4..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod config; -pub mod error; -pub mod frontmatter; -pub mod fs; -pub mod markdown; -pub mod site; -pub mod types; -pub mod util; - -#[cfg(feature = "serve")] -pub mod serve; - -pub use error::RawssgError; -pub use site::TemplateRenderer; -pub use site::builders::site::Site; -pub use site::builders::site_builder::SiteBuilder; -pub use types::RawssgConfig; diff --git a/src/markdown.rs b/src/markdown.rs deleted file mode 100644 index 024f01b..0000000 --- a/src/markdown.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub trait MarkdownRenderer: Send + Sync { - fn render(&self, markdown: &str) -> String; -} - -#[cfg(feature = "pulldown")] -pub struct PulldownMarkdown; - -#[cfg(feature = "pulldown")] -impl MarkdownRenderer for PulldownMarkdown { - fn render(&self, md: &str) -> String { - use pulldown_cmark::{Options, Parser, html}; - let mut options = Options::empty(); - options.insert(Options::ENABLE_TABLES); - options.insert(Options::ENABLE_STRIKETHROUGH); - options.insert(Options::ENABLE_TASKLISTS); - let parser = Parser::new_ext(md, options); - let mut html_out = String::new(); - html::push_html(&mut html_out, parser); - html_out - } -} diff --git a/src/serve/mod.rs b/src/serve/mod.rs deleted file mode 100644 index 19468b1..0000000 --- a/src/serve/mod.rs +++ /dev/null @@ -1,98 +0,0 @@ -#[cfg(feature = "serve")] -mod server { - use crate::error::RawssgError; - use crate::fs::FileSystem; - use crate::fs::real::RealFs; - use crate::util::safe_path; - use std::path::{Path, PathBuf}; - use std::thread; - use tiny_http::{Header, Response, Server}; - - fn mime_type(path: &Path) -> &'static str { - match path.extension().and_then(|e| e.to_str()) { - Some("html") => "text/html; charset=utf-8", - Some("css") => "text/css", - Some("js") => "application/javascript", - Some("json") => "application/json", - Some("xml") => "application/xml", - Some("svg") => "image/svg+xml", - Some("png") => "image/png", - Some("jpg") | Some("jpeg") => "image/jpeg", - Some("gif") => "image/gif", - Some("ico") => "image/x-icon", - Some("woff") => "font/woff", - Some("woff2") => "font/woff2", - Some("ttf") => "font/ttf", - Some("txt") | Some("md") | Some("yaml") | Some("yml") | Some("log") => { - "text/plain; charset=utf-8" - } - _ => "application/octet-stream", - } - } - - fn handle_request(request: tiny_http::Request, dist_path: &Path) -> Result<(), RawssgError> { - let url = request.url().to_string(); - let requested_path = if url == "/" { - "index.html" - } else { - url.trim_start_matches('/') - }; - - let candidate = Path::new(requested_path); - - match safe_path(&RealFs, dist_path, candidate) { - Ok(safe_path) => { - let fs = RealFs; - match fs.read_bytes(&safe_path) { - Ok(content) => { - let mime = mime_type(&safe_path); - let header = Header::from_bytes("Content-Type", mime.as_bytes()) - .unwrap_or_else(|_| { - Header::from_bytes("Content-Type", b"application/octet-stream") - .unwrap() - }); - let response = Response::from_data(content).with_header(header); - request.respond(response).ok(); - } - Err(_) => { - let response = Response::from_string("500 Internal Server Error") - .with_status_code(500); - request.respond(response).ok(); - } - } - } - Err(_) => { - let response = Response::from_string("404 Not Found").with_status_code(404); - request.respond(response).ok(); - } - } - Ok(()) - } - - pub fn start_dev_server(output_dir: &Path, port: u16) -> Result<(), RawssgError> { - let dist: PathBuf = output_dir - .canonicalize() - .unwrap_or_else(|_| output_dir.to_path_buf()); - - let server = Server::http(format!("0.0.0.0:{}", port)) - .map_err(|e| RawssgError::Io(std::io::Error::other(e)))?; - - tracing::info!("dev server listening on http://localhost:{}", port); - for request in server.incoming_requests() { - let dist = dist.clone(); - thread::spawn(move || { - if let Err(e) = handle_request(request, &dist) { - tracing::error!("request error: {:?}", e); - } - }); - } - Ok(()) - } -} - -#[cfg(feature = "serve")] -pub use server::start_dev_server; -#[cfg(feature = "serve")] -pub mod watcher; -#[cfg(feature = "serve")] -pub use watcher::watch_dirs; diff --git a/src/serve/watcher.rs b/src/serve/watcher.rs deleted file mode 100644 index 042e154..0000000 --- a/src/serve/watcher.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[cfg(feature = "serve")] -use notify::{RecursiveMode, Watcher}; -#[cfg(feature = "serve")] -use std::path::PathBuf; -#[cfg(feature = "serve")] -use std::sync::mpsc; - -#[cfg(feature = "serve")] -pub fn watch_dirs(dirs: &[PathBuf], on_change: F) -> notify::Result> -where - F: Fn() + Send + 'static, -{ - let (tx, rx) = mpsc::channel(); - let watcher_tx = tx.clone(); - let mut watcher = notify::recommended_watcher(move |res: notify::Result| { - if let Ok(event) = res - && matches!( - event.kind, - notify::EventKind::Modify(_) - | notify::EventKind::Create(_) - | notify::EventKind::Remove(_) - ) - { - let _ = watcher_tx.send(()); - } - })?; - - for dir in dirs { - if dir.exists() { - watcher.watch(dir, RecursiveMode::Recursive)?; - } - } - - std::thread::spawn(move || { - while rx.recv().is_ok() { - on_change(); - } - }); - - Ok(Box::new(watcher)) -} diff --git a/src/site/builders/mod.rs b/src/site/builders/mod.rs deleted file mode 100644 index ea7a0cd..0000000 --- a/src/site/builders/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod site; -pub mod site_builder; diff --git a/src/site/builders/site.rs b/src/site/builders/site.rs deleted file mode 100644 index b7b1524..0000000 --- a/src/site/builders/site.rs +++ /dev/null @@ -1,314 +0,0 @@ -use crate::error::RawssgError; -use crate::fs::FileSystem; -#[cfg(feature = "tera")] -use crate::site::context::{FeedContextBuilder, SitemapContextBuilder}; -use crate::site::{Context, TemplateRenderer}; -use crate::types::{PageContext, RawssgConfig}; -#[cfg(feature = "tera")] -use crate::util::relative_prefix; -use crate::util::safe_path; -use std::io; -use std::path::{Path, PathBuf}; - -pub struct Site { - pub(crate) config: RawssgConfig, - pub(crate) pages: Vec, - pub(crate) output_dir: PathBuf, - #[cfg_attr(not(feature = "tera"), allow(dead_code))] - pub(crate) base_url: String, - pub(crate) fs: Box, - pub(crate) renderer: Box, - pub(crate) content_dir: PathBuf, - #[cfg(feature = "tera")] - pub(crate) feed_context_builder: Option>, - #[cfg(feature = "tera")] - pub(crate) sitemap_context_builder: Option>, -} - -impl Site { - pub fn pages(&self) -> &[PageContext] { - &self.pages - } - - #[tracing::instrument(skip(self))] - pub fn generate(self) -> Result<(), RawssgError> { - let tmp_dir = self.output_dir.with_extension("tmp"); - if self.fs.exists(&tmp_dir) { - self.fs.remove_dir_all(&tmp_dir)?; - } - self.fs.create_dir_all(&tmp_dir)?; - - self.generate_to(&tmp_dir)?; - - if self.fs.exists(&self.output_dir) { - self.fs.remove_dir_all(&self.output_dir)?; - } - self.fs.rename(&tmp_dir, &self.output_dir).or_else(|e| { - if e.kind() == io::ErrorKind::CrossesDevices { - self.copy_dir_all(&tmp_dir, &self.output_dir)?; - self.fs.remove_dir_all(&tmp_dir)?; - Ok(()) - } else { - Err(RawssgError::SiteGeneration(format!( - "Atomic rename failed: {}", - e - ))) - } - })?; - - Ok(()) - } - - pub(crate) fn generate_to(&self, output_base: &Path) -> Result<(), RawssgError> { - self.fs.create_dir_all(output_base)?; - - let static_dir = self.try_canonicalize_or_skip(Path::new(&self.config.build.static_dir))?; - let content_dir = self.try_canonicalize_or_skip(&self.content_dir)?; - - for page in &self.pages { - if page.is_list { - continue; - } - let ctx = self.build_context(page)?; - let template = self.template_for_page(page); - let html = self.renderer.render(&template, &*ctx)?; - - self.write_page_output(output_base, page, &html)?; - } - - for page in &self.pages { - if !page.is_list { - continue; - } - #[allow(unused_mut)] - let mut ctx = self.build_context(page)?; - if let Some(_items) = &page.list_items { - #[cfg(feature = "tera")] - if let Some(tera_ctx) = ctx.as_mut_any().downcast_mut::() { - tera_ctx.insert("pages", _items); - } - } - let template = self.template_for_page(page); - let html = self.renderer.render(&template, &*ctx)?; - - self.write_page_output(output_base, page, &html)?; - } - - if let Some(ref dir) = static_dir - && self.fs.exists(dir) - { - self.copy_static_assets(dir, output_base)?; - } - if let Some(ref dir) = content_dir { - self.copy_content_assets(dir, output_base)?; - } - - #[cfg(feature = "tera")] - { - if self.config.generators.rss.enabled { - if let Some(ref feed_builder) = self.feed_context_builder { - let blog_posts: Vec<&PageContext> = self - .pages - .iter() - .filter(|p| p.content_type == "blog" && !p.is_list) - .collect(); - if !blog_posts.is_empty() { - let rss = crate::site::feed::generate_feed( - &*self.renderer, - &self.config, - &blog_posts, - &self.base_url, - &**feed_builder, - )?; - let rss_path = Path::new(&self.config.generators.rss.path); - self.write_generated_file(output_base, rss_path, rss.as_bytes())?; - } - } else { - return Err(RawssgError::Internal( - "RSS enabled but no feed context builder provided".into(), - )); - } - } - - if self.config.generators.sitemap.enabled { - if let Some(ref sitemap_builder) = self.sitemap_context_builder { - let sitemap = crate::site::sitemap::generate_sitemap( - &*self.renderer, - &self.config, - &self.pages, - &self.base_url, - &**sitemap_builder, - )?; - let sitemap_path = Path::new(&self.config.generators.sitemap.path); - self.write_generated_file(output_base, sitemap_path, sitemap.as_bytes())?; - } else { - return Err(RawssgError::Internal( - "Sitemap enabled but no sitemap context builder provided".into(), - )); - } - } - } - - Ok(()) - } - - fn write_page_output( - &self, - output_base: &Path, - page: &PageContext, - html: &str, - ) -> Result<(), RawssgError> { - let candidate = Path::new(&page.url); - if let Some(parent_rel) = candidate.parent() { - let parent_out = output_base.join(parent_rel); - self.fs.create_dir_all(&parent_out)?; - } - let out_path = safe_path(self.fs.as_ref(), output_base, candidate)?; - self.fs.write(&out_path, html.as_bytes())?; - Ok(()) - } - #[cfg(feature = "tera")] - fn write_generated_file( - &self, - output_base: &Path, - rel_path: &Path, - content: &[u8], - ) -> Result<(), RawssgError> { - if let Some(parent_rel) = rel_path.parent() { - let parent_out = output_base.join(parent_rel); - self.fs.create_dir_all(&parent_out)?; - } - let out_path = safe_path(self.fs.as_ref(), output_base, rel_path)?; - self.fs.write(&out_path, content)?; - Ok(()) - } - - fn try_canonicalize_or_skip(&self, path: &Path) -> Result, RawssgError> { - match self.fs.canonicalize(path) { - Ok(p) if self.fs.exists(&p) => Ok(Some(p)), - Ok(_) => Ok(None), - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), - Err(e) => Err(RawssgError::SiteGeneration(format!( - "Cannot access directory '{}': {}", - path.display(), - e - ))), - } - } - - #[cfg(feature = "tera")] - fn build_context(&self, page: &PageContext) -> Result, RawssgError> { - let mut ctx = tera::Context::new(); - self.fill_tera_context(page, &mut ctx); - Ok(Box::new(ctx)) - } - - #[cfg(not(feature = "tera"))] - fn build_context(&self, _page: &PageContext) -> Result, RawssgError> { - Err(RawssgError::Internal( - "Context builder not available without 'tera' feature".into(), - )) - } - - #[cfg(feature = "tera")] - fn fill_tera_context(&self, page: &PageContext, ctx: &mut tera::Context) { - ctx.insert("site", &self.config.site); - ctx.insert("base_url", &self.base_url); - ctx.insert("base_path", &relative_prefix(page.depth)); - ctx.insert("page_title", &page.frontmatter.title); - ctx.insert("page_desc", &page.frontmatter.desc); - ctx.insert("page_content", &page.content_html); - ctx.insert( - "page_author", - &page.frontmatter.author.as_deref().unwrap_or(""), - ); - ctx.insert( - "page_repo_url", - &page.frontmatter.repo_url.as_deref().unwrap_or(""), - ); - ctx.insert( - "page_license", - &page.frontmatter.license.as_deref().unwrap_or(""), - ); - ctx.insert("page_url", &page.url); - ctx.insert("page_pub_date", &page.pub_date.as_deref().unwrap_or("")); - } - - fn template_for_page(&self, page: &PageContext) -> String { - for ct in &self.config.content_types { - if ct.name == page.content_type { - if page.is_list - && let Some(ref list_tpl) = ct.list_template - { - return list_tpl.clone(); - } - return ct.template.clone(); - } - } - "base.html".into() - } - - fn copy_static_assets(&self, static_dir: &Path, output_base: &Path) -> Result<(), RawssgError> { - for entry in self.fs.walk_dir(static_dir)? { - let rel = entry - .strip_prefix(static_dir) - .map_err(|e| RawssgError::SiteGeneration(e.to_string()))?; - self.copy_asset_to_output(output_base, rel, &entry)?; - } - Ok(()) - } - - fn copy_content_assets( - &self, - content_dir: &Path, - output_base: &Path, - ) -> Result<(), RawssgError> { - if !self.fs.exists(content_dir) { - return Ok(()); - } - for entry in self.fs.walk_dir(content_dir)? { - if entry.extension().map(|e| e == "md").unwrap_or(false) { - continue; - } - let rel = entry - .strip_prefix(content_dir) - .map_err(|e| RawssgError::SiteGeneration(e.to_string()))?; - self.copy_asset_to_output(output_base, rel, &entry)?; - } - Ok(()) - } - - fn copy_asset_to_output( - &self, - output_base: &Path, - rel: &Path, - source: &Path, - ) -> Result<(), RawssgError> { - if let Some(parent_rel) = rel.parent() { - let parent_out = output_base.join(parent_rel); - self.fs.create_dir_all(&parent_out)?; - } - let dest = safe_path(self.fs.as_ref(), output_base, rel)?; - self.fs.copy_file(source, &dest)?; - Ok(()) - } - - fn copy_dir_all(&self, from: &Path, to: &Path) -> Result<(), RawssgError> { - self.fs.create_dir_all(to)?; - for entry in self.fs.walk_dir(from)? { - let rel = entry - .strip_prefix(from) - .map_err(|e| RawssgError::SiteGeneration(e.to_string()))?; - let dest = to.join(rel); - if self.fs.is_dir(&entry) { - self.fs.create_dir_all(&dest)?; - } else { - if let Some(parent) = dest.parent() { - self.fs.create_dir_all(parent)?; - } - self.fs.copy_file(&entry, &dest)?; - } - } - Ok(()) - } -} diff --git a/src/site/builders/site_builder.rs b/src/site/builders/site_builder.rs deleted file mode 100644 index 206c9b3..0000000 --- a/src/site/builders/site_builder.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::config::ConfigLoader; -use crate::error::RawssgError; -use crate::fs::FileSystem; -use crate::markdown::MarkdownRenderer; -#[cfg(feature = "tera")] -use crate::site::context::{FeedContextBuilder, SitemapContextBuilder}; -use crate::site::{ContentHandler, MarkdownPageHandler, StaticFileHandler, TemplateRenderer}; -use crate::types::{PageContext, RawssgConfig}; -use std::path::{Path, PathBuf}; - -use super::site::Site; - -pub struct SiteBuilder { - config: RawssgConfig, - content_dir: PathBuf, - output_dir: PathBuf, - fs: Box, - md_renderer: Option>, - renderer: Option>, - handlers: Vec>, - #[cfg(feature = "tera")] - feed_context_builder: Option>, - #[cfg(feature = "tera")] - sitemap_context_builder: Option>, -} - -impl SiteBuilder { - pub fn new() -> Self { - Self { - config: RawssgConfig::default(), - content_dir: PathBuf::from("content"), - output_dir: PathBuf::from("dist"), - fs: Box::new(crate::fs::real::RealFs), - md_renderer: None, - renderer: None, - handlers: vec![Box::new(MarkdownPageHandler), Box::new(StaticFileHandler)], - #[cfg(feature = "tera")] - feed_context_builder: None, - #[cfg(feature = "tera")] - sitemap_context_builder: None, - } - } - - pub fn config(mut self, config: RawssgConfig) -> Self { - self.config = config; - self - } - - pub fn load_config + Send + Sync>( - mut self, - path: P, - ) -> Result { - let loader = crate::config::loader::YamlConfigLoader::new(path); - self.config = loader.load()?; - Ok(self) - } - - pub fn content_dir(mut self, dir: impl Into) -> Self { - self.content_dir = dir.into(); - self - } - - pub fn output_dir(mut self, dir: impl Into) -> Self { - self.output_dir = dir.into(); - self - } - - pub fn with_fs(mut self, fs: Box) -> Self { - self.fs = fs; - self - } - - pub fn with_markdown_renderer(mut self, md: Box) -> Self { - self.md_renderer = Some(md); - self - } - - pub fn with_template_renderer(mut self, tr: Box) -> Self { - self.renderer = Some(tr); - self - } - - pub fn add_handler(mut self, handler: Box) -> Self { - self.handlers.push(handler); - self - } - - #[cfg(feature = "tera")] - pub fn with_feed_context_builder(mut self, b: Box) -> Self { - self.feed_context_builder = Some(b); - self - } - - #[cfg(feature = "tera")] - pub fn with_sitemap_context_builder(mut self, b: Box) -> Self { - self.sitemap_context_builder = Some(b); - self - } - - #[tracing::instrument(skip(self))] - pub fn build(mut self) -> Result { - self.config.validate()?; - - let md_renderer = self - .md_renderer - .take() - .ok_or_else(|| RawssgError::Config("markdown renderer not set".into()))?; - let renderer = self - .renderer - .take() - .ok_or_else(|| RawssgError::Config("template renderer not set".into()))?; - - #[cfg(feature = "tera")] - let feed_context_builder = if self.config.generators.rss.enabled { - Some( - self.feed_context_builder - .take() - .ok_or_else(|| RawssgError::Config("feed context builder not set".into()))?, - ) - } else { - None - }; - - #[cfg(feature = "tera")] - let sitemap_context_builder = if self.config.generators.sitemap.enabled { - Some( - self.sitemap_context_builder - .take() - .ok_or_else(|| RawssgError::Config("sitemap context builder not set".into()))?, - ) - } else { - None - }; - - if self.content_dir == Path::new("content") { - self.content_dir = PathBuf::from(&self.config.build.content_dir); - } - if self.output_dir == Path::new("dist") { - self.output_dir = PathBuf::from(&self.config.build.output_dir); - } - - let base_url = self - .config - .site - .base_url - .clone() - .unwrap_or_else(|| "http://localhost:3000".to_string()); - - let mut pages = Vec::new(); - let mut blog_posts = Vec::new(); - - let all_files = self.fs.walk_dir(&self.content_dir)?; - for file_path in &all_files { - let rel = match file_path.strip_prefix(&self.content_dir) { - Ok(r) => r.to_path_buf(), - Err(_) => continue, - }; - for handler in &self.handlers { - if handler.can_handle(&rel, file_path) { - match handler.process(&*self.fs, &*md_renderer, &rel, &self.content_dir) { - Ok(Some(ctx)) => { - let mut ctx = ctx; - ctx.content_type = self.determine_content_type(&rel); - if ctx.content_type == "blog" && !ctx.is_list { - blog_posts.push(ctx.clone()); - } - pages.push(ctx); - } - Ok(None) => {} - Err(e) => { - tracing::error!("Failed to process {}: {}", file_path.display(), e); - } - } - break; - } - } - } - - blog_posts.sort_by(|a, b| { - b.frontmatter - .date - .cmp(&a.frontmatter.date) - .then_with(|| a.frontmatter.title.cmp(&b.frontmatter.title)) - }); - - for ct in &self.config.content_types { - if ct.list_enabled && ct.list_template.is_some() { - let items: Vec = pages - .iter() - .filter(|p| p.content_type == ct.name && !p.is_list) - .cloned() - .collect(); - if !items.is_empty() { - let list_url = format!("{}/index.html", ct.name); - pages.push(PageContext { - url: list_url, - file_path: String::new(), - depth: 1, - pub_date: None, - frontmatter: crate::types::PageFrontMatter { - title: ct.name.clone(), - ..Default::default() - }, - content_html: String::new(), - content_type: ct.name.clone(), - is_list: true, - list_items: Some(items), - }); - } - } - } - - Ok(Site { - config: self.config, - pages, - output_dir: self.output_dir, - base_url, - fs: self.fs, - renderer, - content_dir: self.content_dir, - #[cfg(feature = "tera")] - feed_context_builder, - #[cfg(feature = "tera")] - sitemap_context_builder, - }) - } - - fn determine_content_type(&self, relative_path: &Path) -> String { - for ct in &self.config.content_types { - if crate::util::match_pattern(&ct.pattern, relative_path) { - return ct.name.clone(); - } - } - "page".into() - } -} - -impl Default for SiteBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/src/site/context.rs b/src/site/context.rs deleted file mode 100644 index 595b95e..0000000 --- a/src/site/context.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::error::RawssgError; -use crate::site::Context; -use crate::types::{PageContext, RawssgConfig}; - -pub trait FeedContextBuilder: Send + Sync { - fn build_feed_context( - &self, - config: &RawssgConfig, - posts: &[&PageContext], - base_url: &str, - ) -> Result, RawssgError>; -} - -pub trait SitemapContextBuilder: Send + Sync { - fn build_sitemap_context( - &self, - config: &RawssgConfig, - pages: &[PageContext], - base_url: &str, - ) -> Result, RawssgError>; -} - -#[cfg(feature = "tera")] -pub struct TeraFeedContextBuilder; - -#[cfg(feature = "tera")] -impl FeedContextBuilder for TeraFeedContextBuilder { - fn build_feed_context( - &self, - config: &RawssgConfig, - posts: &[&PageContext], - base_url: &str, - ) -> Result, RawssgError> { - let mut ctx = tera::Context::new(); - ctx.insert("site", &config.site); - ctx.insert("posts", posts); - ctx.insert("base_url", base_url); - Ok(Box::new(ctx)) - } -} - -#[cfg(feature = "tera")] -pub struct TeraSitemapContextBuilder; - -#[cfg(feature = "tera")] -impl SitemapContextBuilder for TeraSitemapContextBuilder { - fn build_sitemap_context( - &self, - config: &RawssgConfig, - pages: &[PageContext], - base_url: &str, - ) -> Result, RawssgError> { - let mut ctx = tera::Context::new(); - ctx.insert("site", &config.site); - ctx.insert("pages", pages); - ctx.insert("base_url", base_url); - Ok(Box::new(ctx)) - } -} diff --git a/src/site/feed.rs b/src/site/feed.rs deleted file mode 100644 index 27b7b71..0000000 --- a/src/site/feed.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(feature = "tera")] -use super::context::FeedContextBuilder; -#[cfg(feature = "tera")] -use crate::error::RawssgError; -#[cfg(feature = "tera")] -use crate::site::TemplateRenderer; -#[cfg(feature = "tera")] -use crate::types::{PageContext, RawssgConfig}; - -#[cfg(feature = "tera")] -#[tracing::instrument(skip(renderer, context_builder))] -pub fn generate_feed( - renderer: &dyn TemplateRenderer, - config: &RawssgConfig, - posts: &[&PageContext], - base_url: &str, - context_builder: &dyn FeedContextBuilder, -) -> Result { - let ctx = context_builder.build_feed_context(config, posts, base_url)?; - renderer.render(&config.generators.rss.template, &*ctx) -} diff --git a/src/site/mod.rs b/src/site/mod.rs deleted file mode 100644 index 1bf6ab5..0000000 --- a/src/site/mod.rs +++ /dev/null @@ -1,116 +0,0 @@ -pub mod builders; -pub mod feed; -pub mod page; -pub mod sitemap; - -use crate::error::RawssgError; -use crate::fs::FileSystem; -use crate::markdown::MarkdownRenderer; -use crate::types::PageContext; -use std::path::Path; - -pub trait TemplateRenderer: Send + Sync { - fn render(&self, template_name: &str, context: &dyn Context) -> Result; -} - -pub trait Context: Send + Sync { - fn as_any(&self) -> &dyn std::any::Any; - fn as_mut_any(&mut self) -> &mut dyn std::any::Any; -} - -#[cfg(feature = "tera")] -pub mod context; - -#[cfg(feature = "tera")] -pub struct TeraRenderer { - tera: tera::Tera, -} - -#[cfg(feature = "tera")] -impl TeraRenderer { - pub fn new() -> Self { - Self { - tera: tera::Tera::default(), - } - } - - pub fn add_raw_template(&mut self, name: &str, content: &str) -> Result<(), RawssgError> { - self.tera - .add_raw_template(name, content) - .map_err(|e| RawssgError::Template(e.to_string())) - } -} - -#[cfg(feature = "tera")] -impl Default for TeraRenderer { - fn default() -> Self { - Self::new() - } -} - -#[cfg(feature = "tera")] -impl TemplateRenderer for TeraRenderer { - fn render(&self, template: &str, context: &dyn Context) -> Result { - let ctx = context - .as_any() - .downcast_ref::() - .ok_or_else(|| RawssgError::Template("Invalid context type for Tera".into()))?; - self.tera - .render(template, ctx) - .map_err(|e| RawssgError::Template(e.to_string())) - } -} - -#[cfg(feature = "tera")] -impl Context for tera::Context { - fn as_any(&self) -> &dyn std::any::Any { - self - } - fn as_mut_any(&mut self) -> &mut dyn std::any::Any { - self - } -} - -pub trait ContentHandler: Send + Sync { - fn can_handle(&self, relative_path: &Path, original_path: &Path) -> bool; - fn process( - &self, - fs: &dyn FileSystem, - md_renderer: &dyn MarkdownRenderer, - file_path: &Path, - content_dir: &Path, - ) -> Result, RawssgError>; -} - -pub struct MarkdownPageHandler; -impl ContentHandler for MarkdownPageHandler { - fn can_handle(&self, _rel: &Path, orig: &Path) -> bool { - orig.extension().is_some_and(|e| e == "md") - } - - fn process( - &self, - fs: &dyn FileSystem, - md_renderer: &dyn MarkdownRenderer, - file_path: &Path, - content_dir: &Path, - ) -> Result, RawssgError> { - crate::site::page::build_page_context(fs, md_renderer, file_path, content_dir) - } -} - -pub struct StaticFileHandler; -impl ContentHandler for StaticFileHandler { - fn can_handle(&self, _rel: &Path, _orig: &Path) -> bool { - true - } - fn process( - &self, - _fs: &dyn FileSystem, - _md_renderer: &dyn MarkdownRenderer, - _file_path: &Path, - _content_dir: &Path, - ) -> Result, RawssgError> { - Ok(None) - } -} diff --git a/src/site/page.rs b/src/site/page.rs deleted file mode 100644 index 3ef5478..0000000 --- a/src/site/page.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::error::RawssgError; -use crate::frontmatter::parse_frontmatter_and_render; -use crate::fs::FileSystem; -use crate::markdown::MarkdownRenderer; -use crate::types::PageContext; -use crate::util::safe_path; -use chrono::{NaiveTime, TimeZone, Utc}; -use std::path::Path; - -#[tracing::instrument(skip(fs, markdown_renderer))] -pub fn build_page_context( - fs: &dyn FileSystem, - markdown_renderer: &dyn MarkdownRenderer, - file_path: &Path, - content_dir: &Path, -) -> Result, RawssgError> { - let safe_file_path = safe_path(fs, content_dir, file_path)?; - let raw = fs.read_to_string(&safe_file_path)?; - - let (fm, content_html) = - parse_frontmatter_and_render(&raw, &safe_file_path, markdown_renderer)?; - - if fm.draft { - tracing::warn!("Skipping draft: {}", safe_file_path.display()); - return Ok(None); - } - - let base_canon = fs - .canonicalize(content_dir) - .map_err(|e| RawssgError::SiteGeneration(format!("canonicalize base: {}", e)))?; - let rel_path = safe_file_path - .strip_prefix(&base_canon) - .map_err(|_| RawssgError::SiteGeneration("prefix not found".into()))?; - - let url = rel_path - .with_extension("html") - .to_string_lossy() - .to_string(); - let depth = rel_path.components().count().saturating_sub(1); - - let pub_date = fm.date.map(|d| { - Utc.from_utc_datetime(&d.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())) - .format("%a, %d %b %Y %H:%M:%S %z") - .to_string() - }); - - Ok(Some(PageContext { - frontmatter: fm, - content_html, - url, - file_path: safe_file_path.to_string_lossy().to_string(), - depth, - pub_date, - content_type: "page".into(), - is_list: false, - list_items: None, - })) -} diff --git a/src/site/sitemap.rs b/src/site/sitemap.rs deleted file mode 100644 index 116b378..0000000 --- a/src/site/sitemap.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(feature = "tera")] -use super::context::SitemapContextBuilder; -#[cfg(feature = "tera")] -use crate::error::RawssgError; -#[cfg(feature = "tera")] -use crate::site::TemplateRenderer; -#[cfg(feature = "tera")] -use crate::types::{PageContext, RawssgConfig}; - -#[cfg(feature = "tera")] -#[tracing::instrument(skip(renderer, context_builder))] -pub fn generate_sitemap( - renderer: &dyn TemplateRenderer, - config: &RawssgConfig, - pages: &[PageContext], - base_url: &str, - context_builder: &dyn SitemapContextBuilder, -) -> Result { - let ctx = context_builder.build_sitemap_context(config, pages, base_url)?; - renderer.render(&config.generators.sitemap.template, &*ctx) -} diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 9ba35dc..0000000 --- a/src/types.rs +++ /dev/null @@ -1,246 +0,0 @@ -use chrono::NaiveDate; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RawssgConfig { - #[serde(default)] - pub site: GlobalConfig, - #[serde(default)] - pub build: BuildConfig, - #[serde(default)] - pub content_types: Vec, - #[serde(default)] - pub generators: GeneratorsConfig, -} - -impl RawssgConfig { - pub fn validate(&self) -> Result<(), crate::error::RawssgError> { - if self.site.site_name.trim().is_empty() { - return Err(crate::error::RawssgError::Config( - "site.name cannot be empty".into(), - )); - } - - if self.content_types.is_empty() { - return Err(crate::error::RawssgError::Config( - "at least one content_type must be defined".into(), - )); - } - - for ct in &self.content_types { - if ct.name.trim().is_empty() { - return Err(crate::error::RawssgError::Config(format!( - "content_type name cannot be empty (pattern: {})", - ct.pattern - ))); - } - if ct.template.trim().is_empty() { - return Err(crate::error::RawssgError::Config(format!( - "content_type '{}' must have a template", - ct.name - ))); - } - if let Err(e) = glob::Pattern::new(&ct.pattern) { - return Err(crate::error::RawssgError::Config(format!( - "Invalid glob pattern '{}' for content_type '{}': {}", - ct.pattern, ct.name, e - ))); - } - } - - if self.generators.rss.enabled { - if self.generators.rss.template.trim().is_empty() { - return Err(crate::error::RawssgError::Config( - "rss.template is required when RSS enabled".into(), - )); - } - if self.generators.rss.path.trim().is_empty() { - return Err(crate::error::RawssgError::Config( - "rss.path is required when RSS enabled".into(), - )); - } - } - - if self.generators.sitemap.enabled { - if self.generators.sitemap.template.trim().is_empty() { - return Err(crate::error::RawssgError::Config( - "sitemap.template is required when sitemap enabled".into(), - )); - } - if self.generators.sitemap.path.trim().is_empty() { - return Err(crate::error::RawssgError::Config( - "sitemap.path is required when sitemap enabled".into(), - )); - } - } - - Ok(()) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct GlobalConfig { - #[serde(default)] - pub navbar: Vec, - #[serde(default)] - pub sidebar: Vec, - #[serde(default = "default_site_name")] - pub site_name: String, - #[serde(default)] - pub description: Option, - #[serde(default)] - pub language: Option, - #[serde(default)] - pub base_url: Option, - #[serde(default)] - pub author: Option, - #[serde(default)] - pub repo_url: Option, - #[serde(default)] - pub license: Option, -} - -impl Default for GlobalConfig { - fn default() -> Self { - Self { - navbar: vec![], - sidebar: vec![], - site_name: default_site_name(), - description: None, - language: Some("en".into()), - base_url: None, - author: None, - repo_url: None, - license: None, - } - } -} - -fn default_site_name() -> String { - "rawssg".into() -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct BuildConfig { - #[serde(default = "default_content_dir")] - pub content_dir: String, - #[serde(default = "default_output_dir")] - pub output_dir: String, - #[serde(default = "default_templates_dir")] - pub templates_dir: String, - #[serde(default = "default_static_dir")] - pub static_dir: String, -} - -fn default_content_dir() -> String { - "content".into() -} -fn default_output_dir() -> String { - "dist".into() -} -fn default_templates_dir() -> String { - "templates".into() -} -fn default_static_dir() -> String { - "static".into() -} - -impl Default for BuildConfig { - fn default() -> Self { - Self { - content_dir: default_content_dir(), - output_dir: default_output_dir(), - templates_dir: default_templates_dir(), - static_dir: default_static_dir(), - } - } -} - -impl Default for RawssgConfig { - fn default() -> Self { - Self { - site: GlobalConfig::default(), - build: BuildConfig::default(), - content_types: vec![ContentTypeDef { - name: "page".into(), - pattern: "**/*.md".into(), - template: "base.html".into(), - list_template: None, - list_enabled: false, - }], - generators: GeneratorsConfig::default(), - } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ContentTypeDef { - pub name: String, - pub pattern: String, - pub template: String, - #[serde(default)] - pub list_template: Option, - #[serde(default)] - pub list_enabled: bool, -} - -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -pub struct GeneratorsConfig { - #[serde(default)] - pub rss: GeneratorDef, - #[serde(default)] - pub sitemap: GeneratorDef, -} - -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -pub struct GeneratorDef { - #[serde(default = "default_false")] - pub enabled: bool, - #[serde(default)] - pub path: String, - #[serde(default)] - pub template: String, -} - -fn default_false() -> bool { - false -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct NavItem { - pub label: String, - pub url: String, - #[serde(default)] - pub children: Vec, -} - -#[derive(Debug, Deserialize, Serialize, Clone, Default)] -pub struct PageFrontMatter { - pub title: String, - pub desc: String, - #[serde(default)] - pub author: Option, - #[serde(default)] - pub repo_url: Option, - #[serde(default)] - pub license: Option, - #[serde(default)] - pub date: Option, - #[serde(default)] - pub tags: Vec, - #[serde(default)] - pub draft: bool, -} - -#[derive(Debug, Serialize, Clone)] -pub struct PageContext { - pub frontmatter: PageFrontMatter, - pub content_html: String, - pub url: String, - pub file_path: String, - pub depth: usize, - pub pub_date: Option, - pub content_type: String, - pub is_list: bool, - pub list_items: Option>, -} diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index a56ff8b..0000000 --- a/src/util.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::error::RawssgError; -use crate::fs::FileSystem; -use std::path::{Component, Path, PathBuf}; - -fn normalize_path(path: &Path) -> PathBuf { - let mut components = Vec::new(); - for comp in path.components() { - match comp { - Component::ParentDir => { - if components - .last() - .is_some_and(|c: &Component| c != &Component::RootDir) - { - components.pop(); - } - } - Component::CurDir => {} - other => components.push(other), - } - } - components.into_iter().collect() -} - -pub fn safe_path( - fs: &dyn FileSystem, - base: &Path, - candidate: &Path, -) -> Result { - let base_canon = fs.canonicalize(base).map_err(|e| { - RawssgError::Io(std::io::Error::other(format!( - "Cannot canonicalize base '{}': {}", - base.display(), - e - ))) - })?; - - let joined = if candidate.is_relative() { - base_canon.join(candidate) - } else { - candidate.to_path_buf() - }; - let normalized = normalize_path(&joined); - - if fs.exists(&normalized) { - let resolved = fs.canonicalize(&normalized).map_err(|e| { - RawssgError::Io(std::io::Error::other(format!( - "Cannot resolve path '{}': {}", - normalized.display(), - e - ))) - })?; - if !resolved.starts_with(&base_canon) { - return Err(RawssgError::PathTraversal(format!( - "Path traversal detected: {}", - resolved.display() - ))); - } - Ok(resolved) - } else { - let parent = normalized.parent().ok_or_else(|| { - RawssgError::PathTraversal(format!( - "Cannot determine parent of output path: {}", - normalized.display() - )) - })?; - let parent_canon = fs.canonicalize(parent).map_err(|e| { - RawssgError::Io(std::io::Error::other(format!( - "Cannot resolve parent '{}': {}", - parent.display(), - e - ))) - })?; - if !parent_canon.starts_with(&base_canon) { - return Err(RawssgError::PathTraversal(format!( - "Path escapes base: {}", - normalized.display() - ))); - } - let file_name = normalized.file_name().ok_or_else(|| { - RawssgError::PathTraversal(format!( - "Output path has no file name: {}", - normalized.display() - )) - })?; - Ok(parent_canon.join(file_name)) - } -} -pub fn slugify(title: &str) -> String { - title - .to_lowercase() - .chars() - .map(|c| { - if c.is_alphanumeric() || c == '-' { - c - } else { - '-' - } - }) - .collect::() - .split('-') - .filter(|s| !s.is_empty()) - .collect::>() - .join("-") -} - -pub fn relative_prefix(depth: usize) -> String { - if depth == 0 { - "./".into() - } else { - "../".repeat(depth) - } -} - -pub fn match_pattern(pattern: &str, path: &Path) -> bool { - let path_str = path.to_string_lossy(); - let segments: Vec<&str> = path_str.split('/').collect(); - let pattern_segments: Vec<&str> = pattern.split('/').collect(); - - match_pattern_slice(&pattern_segments, &segments) -} - -fn match_pattern_slice(pattern: &[&str], segments: &[&str]) -> bool { - if pattern.is_empty() { - return segments.is_empty(); - } - if segments.is_empty() { - return pattern.iter().all(|&p| p == "**"); - } - - match pattern[0] { - "**" => { - if pattern.len() == 1 { - return true; - } - for i in 0..segments.len() { - if match_pattern_slice(&pattern[1..], &segments[i..]) { - return true; - } - } - false - } - pat => { - if !segment_matches(pat, segments[0]) { - return false; - } - match_pattern_slice(&pattern[1..], &segments[1..]) - } - } -} - -fn segment_matches(pattern: &str, segment: &str) -> bool { - let mut pattern_chars = pattern.chars(); - let mut segment_chars = segment.chars(); - - loop { - match pattern_chars.next() { - Some('*') => { - let rest_of_pattern: String = pattern_chars.clone().collect(); - if rest_of_pattern.is_empty() { - return true; - } - let mut remaining_segment: String = segment_chars.clone().collect(); - while !remaining_segment.is_empty() { - if segment_matches(&rest_of_pattern, &remaining_segment) { - return true; - } - segment_chars.next(); - remaining_segment = segment_chars.clone().collect(); - } - return false; - } - Some(pc) => match segment_chars.next() { - Some(sc) if pc == sc => continue, - _ => return false, - }, - None => return segment_chars.next().is_none(), - } - } -} From f17e65ada331c01b365555c4030d5bf18ea51448 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 01:07:06 +0700 Subject: [PATCH 03/20] feat(workspace): add config, error, fs crates (#26) * chore(lock): update lockfile for new workspace crates Add entries for librawssg_config, librawssg_error, librawssg_fs and adjust existing crate versions to 1.0.0. Include thiserror and its transitive dependencies. * feat(workspace): add config, error, fs crates to workspace Expand workspace members to include librawssg_config, librawssg_error, and librawssg_fs. Keep existing members. * chore(compiler): bump version to 1.0.0 and add metadata Set version to 1.0.0 and add description, license, repository, readme, keywords, and categories. * docs(compiler): add README for compiler crate Add a short description of the build pipeline orchestration role. * feat(config): add config crate manifest Create Cargo.toml for librawssg_config with description, license, repository, readme, keywords, and categories. Include workspace lints. * docs(config): add README for config crate Describe the purpose of the config crate. * feat(config): add placeholder library for config crate Add a simple placeholder lib.rs with add function and test to bootstrap the crate. * feat(error): add error crate manifest Create Cargo.toml for librawssg_error with thiserror dependency and metadata. Use workspace lints. * docs(error): add README for error crate Describe the unified error type and Result alias. * feat(error): define unified error enum and Result alias Add a non-exhaustive Error enum with variants for common SSG failures. Export Result type using thiserror. * test(error): add placeholder integration test file Create empty integration_tests.rs for future tests. * test(error): add placeholder property test file Create empty property_tests.rs for future tests. * test(error): add placeholder unit test file Create empty unit_tests.rs for future tests. * feat(fs): add fs crate manifest Create Cargo.toml for librawssg_fs with description, license, repository, readme, keywords, and categories. Use workspace lints. * docs(fs): add README for fs crate Describe the filesystem abstraction layer. * feat(fs): add placeholder library for fs crate Add a simple placeholder lib.rs with add function and test. * chore(handler): bump version to 1.0.0 and add metadata Set version to 1.0.0 and add description, license, repository, readme, keywords, and categories. * docs(handler): add README for handler crate Describe the content processing contracts. * chore(templates): bump version to 1.0.0 and add metadata Set version to 1.0.0 and add description, license, repository, readme, keywords, and categories. * docs(templates): add README for templates crate Describe the template rendering contracts. --- Cargo.lock | 76 +++++++++++++++++++++- Cargo.toml | 2 +- librawssg_compiler/Cargo.toml | 8 ++- librawssg_compiler/README.md | 4 ++ librawssg_config/Cargo.toml | 15 +++++ librawssg_config/README.md | 4 ++ librawssg_config/src/lib.rs | 14 ++++ librawssg_error/Cargo.toml | 16 +++++ librawssg_error/README.md | 4 ++ librawssg_error/src/lib.rs | 58 +++++++++++++++++ librawssg_error/tests/integration_tests.rs | 1 + librawssg_error/tests/property_tests.rs | 1 + librawssg_error/tests/unit_tests.rs | 1 + librawssg_fs/Cargo.toml | 15 +++++ librawssg_fs/README.md | 4 ++ librawssg_fs/src/lib.rs | 14 ++++ librawssg_handler/Cargo.toml | 8 ++- librawssg_handler/README.md | 4 ++ librawssg_templates/Cargo.toml | 8 ++- librawssg_templates/README.md | 4 ++ 20 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 librawssg_compiler/README.md create mode 100644 librawssg_config/Cargo.toml create mode 100644 librawssg_config/README.md create mode 100644 librawssg_config/src/lib.rs create mode 100644 librawssg_error/Cargo.toml create mode 100644 librawssg_error/README.md create mode 100644 librawssg_error/src/lib.rs create mode 100644 librawssg_error/tests/integration_tests.rs create mode 100644 librawssg_error/tests/property_tests.rs create mode 100644 librawssg_error/tests/unit_tests.rs create mode 100644 librawssg_fs/Cargo.toml create mode 100644 librawssg_fs/README.md create mode 100644 librawssg_fs/src/lib.rs create mode 100644 librawssg_handler/README.md create mode 100644 librawssg_templates/README.md diff --git a/Cargo.lock b/Cargo.lock index cb330d0..84f6ee0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,12 +4,82 @@ version = 4 [[package]] name = "librawssg_compiler" -version = "0.1.0" +version = "1.0.0" + +[[package]] +name = "librawssg_config" +version = "1.0.0" + +[[package]] +name = "librawssg_error" +version = "1.0.0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "librawssg_fs" +version = "1.0.0" [[package]] name = "librawssg_handler" -version = "0.1.0" +version = "1.0.0" [[package]] name = "librawssg_templates" -version = "0.1.0" +version = "1.0.0" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/Cargo.toml b/Cargo.toml index bb365b8..9fcbab3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["librawssg_compiler","librawssg_handler", "librawssg_templates"] +members = ["librawssg_compiler", "librawssg_config", "librawssg_error", "librawssg_fs","librawssg_handler", "librawssg_templates"] [workspace.lints.clippy] all = { level = "deny", priority = -1 } diff --git a/librawssg_compiler/Cargo.toml b/librawssg_compiler/Cargo.toml index d31f713..8590eaf 100644 --- a/librawssg_compiler/Cargo.toml +++ b/librawssg_compiler/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "librawssg_compiler" -version = "0.1.0" +version = "1.0.0" edition = "2024" +description = "Build pipeline orchestration for the librawssg static site generator" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "static-site-generator", "pipeline", "builder", "compiler"] +categories = ["development-tools::build-utils"] [dependencies] diff --git a/librawssg_compiler/README.md b/librawssg_compiler/README.md new file mode 100644 index 0000000..656345f --- /dev/null +++ b/librawssg_compiler/README.md @@ -0,0 +1,4 @@ +# librawssg_compiler + +Orchestrates the build pipeline for librawssg. +Combines filesystem, processors, renderers, and generators to produce a static site. diff --git a/librawssg_config/Cargo.toml b/librawssg_config/Cargo.toml new file mode 100644 index 0000000..9585972 --- /dev/null +++ b/librawssg_config/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "librawssg_config" +version = "1.0.0" +edition = "2024" +description = "Configuration types and validation for the librawssg static site generator" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "static-site-generator", "config", "yaml", "validation"] +categories = ["development-tools::build-utils"] + +[dependencies] + +[lints] +workspace = true diff --git a/librawssg_config/README.md b/librawssg_config/README.md new file mode 100644 index 0000000..466003a --- /dev/null +++ b/librawssg_config/README.md @@ -0,0 +1,4 @@ +# librawssg_config + +Configuration types and validation for librawssg. +Provides `Config`, site metadata, build settings, and content rule definitions. diff --git a/librawssg_config/src/lib.rs b/librawssg_config/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/librawssg_config/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/librawssg_error/Cargo.toml b/librawssg_error/Cargo.toml new file mode 100644 index 0000000..4af6213 --- /dev/null +++ b/librawssg_error/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "librawssg_error" +version = "1.0.0" +edition = "2024" +description = "Unified error types for the librawssg static site generator" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "static-site-generator", "error", "thiserror"] +categories = ["development-tools::build-utils"] + +[dependencies] +thiserror = "2.0.20" + +[lints] +workspace = true diff --git a/librawssg_error/README.md b/librawssg_error/README.md new file mode 100644 index 0000000..07aee98 --- /dev/null +++ b/librawssg_error/README.md @@ -0,0 +1,4 @@ +# librawssg_error + +Unified error types for the librawssg static site generator ecosystem. +Provides a single `Error` enum and `Result` alias used by all other crates. diff --git a/librawssg_error/src/lib.rs b/librawssg_error/src/lib.rs new file mode 100644 index 0000000..2373b44 --- /dev/null +++ b/librawssg_error/src/lib.rs @@ -0,0 +1,58 @@ +use core::error::Error as CoreError; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("Failed to parse metadata in {path}")] + Metadata { + path: PathBuf, + #[source] + source: Box, + }, + + #[error("Template rendering error: {0}")] + Render(String), + + #[error("Content processor error: {0}")] + Processor(String), + + #[error("Generator error: {0}")] + Generator(String), + + #[error("Path traversal attempt detected: {0}")] + PathTraversal(String), + + #[error("Missing configuration key: {0}")] + MissingConfig(String), + + #[error("Site generation error: {0}")] + Generation(String), + + #[error("Resource not found: {0}")] + NotFound(String), + + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("Validation error: {0}")] + Validation(String), + + #[error("Duplicate value: {0}")] + Duplicate(String), + + #[error("Invalid state: {0}")] + InvalidState(String), + + #[error("Internal error: {0}")] + Internal(String), +} + +pub type Result = core::result::Result; diff --git a/librawssg_error/tests/integration_tests.rs b/librawssg_error/tests/integration_tests.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/librawssg_error/tests/integration_tests.rs @@ -0,0 +1 @@ + diff --git a/librawssg_error/tests/property_tests.rs b/librawssg_error/tests/property_tests.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/librawssg_error/tests/property_tests.rs @@ -0,0 +1 @@ + diff --git a/librawssg_error/tests/unit_tests.rs b/librawssg_error/tests/unit_tests.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/librawssg_error/tests/unit_tests.rs @@ -0,0 +1 @@ + diff --git a/librawssg_fs/Cargo.toml b/librawssg_fs/Cargo.toml new file mode 100644 index 0000000..05b1145 --- /dev/null +++ b/librawssg_fs/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "librawssg_fs" +version = "1.0.0" +edition = "2024" +description = "Filesystem abstraction for the librawssg static site generator" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "static-site-generator", "filesystem", "io"] +categories = ["development-tools::build-utils"] + +[dependencies] + +[lints] +workspace = true diff --git a/librawssg_fs/README.md b/librawssg_fs/README.md new file mode 100644 index 0000000..3ca0a9b --- /dev/null +++ b/librawssg_fs/README.md @@ -0,0 +1,4 @@ +# librawssg_fs + +Filesystem abstraction layer for librawssg. +Defines the `FileSystem` trait and provides a real implementation. diff --git a/librawssg_fs/src/lib.rs b/librawssg_fs/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/librawssg_fs/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/librawssg_handler/Cargo.toml b/librawssg_handler/Cargo.toml index ca51fb3..c4700d7 100644 --- a/librawssg_handler/Cargo.toml +++ b/librawssg_handler/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "librawssg_handler" -version = "0.1.0" +version = "1.0.0" edition = "2024" +description = "Content processing contracts for the librawssg static site generator" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "static-site-generator", "content", "processor", "traits"] +categories = ["development-tools::build-utils"] [dependencies] diff --git a/librawssg_handler/README.md b/librawssg_handler/README.md new file mode 100644 index 0000000..581174e --- /dev/null +++ b/librawssg_handler/README.md @@ -0,0 +1,4 @@ +# librawssg_handler + +Content processing contracts for librawssg. +Defines the `Processor` trait and related types for turning source files into documents. diff --git a/librawssg_templates/Cargo.toml b/librawssg_templates/Cargo.toml index 6900f48..6fecaf7 100644 --- a/librawssg_templates/Cargo.toml +++ b/librawssg_templates/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "librawssg_templates" -version = "0.1.0" +version = "1.0.0" edition = "2024" +description = "Template rendering contracts for the librawssg static site generator" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "static-site-generator", "template", "renderer", "traits"] +categories = ["development-tools::build-utils"] [dependencies] diff --git a/librawssg_templates/README.md b/librawssg_templates/README.md new file mode 100644 index 0000000..81b90c1 --- /dev/null +++ b/librawssg_templates/README.md @@ -0,0 +1,4 @@ +# librawssg_templates + +Template rendering contracts for librawssg. +Defines the `Renderer` and `RenderContext` traits for pluggable template engines. From cc2d16a3fc2d885ce2af25e9331d753b2fa9572a Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 01:17:02 +0700 Subject: [PATCH 04/20] ci: update workflows and makefile, add error tests (#27) * ci(workflows): improve CI pipeline and add build steps Update CI workflow with explicit job names and timeouts. Pin Rust toolchain to 1.96.0. Add build steps for debug and release, ensure formatting check runs in both jobs, and use workspace-wide clippy and test commands. * ci(workflows): remove obsolete docs deployment workflow Delete the GitHub Pages documentation deployment workflow. It is no longer needed since the workspace is being restructured and documentation deployment will be handled separately. * chore(gitignore): ignore generated PR body files Add pull_request_body.md to .gitignore to prevent temporary PR body files from being tracked accidentally. * refactor(makefile): expand build automation with per-crate targets Replace simple Makefile with comprehensive workspace targets. Add per-crate shortcuts, publish helpers, clippy flags variable, and improved help. This improves developer workflow across the new workspace. * test(error): add integration tests for error propagation Add tests verifying Io error conversion via From and Metadata error source accessibility using thiserror. * test(error): add property tests for error messages Add property tests ensuring Config, PathTraversal, and Render error messages preserve their input strings. * test(error): add unit tests for error enum Add comprehensive unit tests covering Display, source, From conversion, and Debug output for all error variants. --- .github/workflows/ci.yml | 46 +++- .github/workflows/docs.yml | 65 ----- .gitignore | 3 +- Makefile | 274 ++++++++++++++++++--- librawssg_error/tests/integration_tests.rs | 32 +++ librawssg_error/tests/property_tests.rs | 51 ++++ librawssg_error/tests/unit_tests.rs | 155 ++++++++++++ 7 files changed, 518 insertions(+), 108 deletions(-) delete mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec4f3f9..30311a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,13 +11,18 @@ env: jobs: base: + name: Base checks (no features) runs-on: ubuntu-latest + timeout-minutes: 30 + steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: + toolchain: "1.96.0" components: rustfmt, clippy - name: Cache Cargo artifacts @@ -31,24 +36,32 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-base- - - name: Check formatting + - name: Check code formatting run: cargo fmt --all -- --check - - name: Lint (no features) - run: cargo clippy --all-targets -- -D warnings + - name: Run Clippy (no features) + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Build (debug, no features) + run: cargo build --workspace --all-targets - - name: Test (no features) - run: cargo test --workspace --verbose + - name: Run tests (no features) + run: cargo test --workspace --all-targets --verbose features: + name: Full feature checks runs-on: ubuntu-latest + timeout-minutes: 30 + steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: - components: clippy + toolchain: "1.96.0" + components: rustfmt, clippy - name: Cache Cargo artifacts uses: actions/cache@v4 @@ -61,8 +74,17 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-features- - - name: Lint (all features) - run: cargo clippy --all-targets --all-features -- -D warnings + - name: Check code formatting + run: cargo fmt --all -- --check + + - name: Run Clippy (all features) + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + - name: Build (debug, all features) + run: cargo build --workspace --all-targets --all-features + + - name: Run tests (all features) + run: cargo test --workspace --all-targets --all-features --verbose - - name: Test (all features) - run: cargo test --workspace --verbose --all-features + - name: Build (release, all features) + run: cargo build --workspace --all-targets --all-features --release diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 3398b4c..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Deploy Docs to GitHub Pages - -on: - push: - branches: ["master"] - paths: - - "docs/**" - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Cache Cargo - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Build documentation site - run: | - cd docs - cargo run - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: "docs/dist" - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 5eee747..1468f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /dev .velodiff_dist.diff .snapcat.md -/dist \ No newline at end of file +/dist +pull_request_body.md \ No newline at end of file diff --git a/Makefile b/Makefile index 4972479..c3ba25c 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,263 @@ -.PHONY: all build release check test lint fmt clippy clean run install ci +SHELL = /bin/bash +.SHELLFLAGS = -euo pipefail -c +CARGO = cargo +MEMBERS = librawssg_error librawssg_fs librawssg_config librawssg_handler librawssg_templates librawssg_compiler +PUBLISH_ORDER = librawssg_error librawssg_fs librawssg_config librawssg_handler librawssg_templates librawssg_compiler + +PKG ?= librawssg_handler + +CLIPPY_FLAGS ?= -- -D warnings + +.DEFAULT_GOAL := help + +.PHONY: help +help: + @echo "Usage: make [PKG=] [CLIPPY_FLAGS=]" + @echo "" + @echo "Targets:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' + @echo "" + @echo "Contoh:" + @echo " make ci" + @echo " make clippy CLIPPY_FLAGS=''" + @echo " make test-pkg PKG=librawssg_core" + +.PHONY: all all: build -build: - cargo build +.PHONY: build +build: + $(CARGO) build --workspace + +.PHONY: release +release: + $(CARGO) build --release --workspace + +.PHONY: check +check: + $(CARGO) check --workspace + +.PHONY: check-all +check-all: + $(CARGO) check --workspace --all-targets --all-features + +.PHONY: test +test: + $(CARGO) test --workspace + +.PHONY: test-all +test-all: test + +.PHONY: test-verbose +test-verbose: + RUST_BACKTRACE=1 $(CARGO) test --workspace -- --nocapture -release: - cargo build --release +.PHONY: watch-test +watch-test: + $(CARGO) watch -x 'test --workspace' -check: - cargo check --workspace +.PHONY: watch-build +watch-build: + $(CARGO) watch -x 'check --workspace' -test: - cargo test --workspace +.PHONY: fmt +fmt: + $(CARGO) fmt --all -test-verbose: - cargo test --workspace -- --nocapture +.PHONY: fmt-check +fmt-check: + $(CARGO) fmt --all -- --check -fmt: - cargo fmt --all +.PHONY: clippy +clippy: + $(CARGO) clippy --workspace --all-targets --all-features $(CLIPPY_FLAGS) -fmt-check: - cargo fmt --all -- --check +.PHONY: clippy-all +clippy-all: clippy -clippy: - cargo clippy -- -D warnings +.PHONY: clippy-strict +clippy-strict: + $(CARGO) clippy --workspace --all-targets --all-features -- -D warnings +.PHONY: lint lint: fmt clippy -clean: - cargo clean +.PHONY: ci +ci: fmt-check clippy test-verbose + +.PHONY: ci-fast +ci-fast: fmt-check clippy test + +.PHONY: clean +clean: + $(CARGO) clean + +.PHONY: doc +doc: + $(CARGO) doc --workspace --no-deps + +.PHONY: doc-open +doc-open: doc + $(CARGO) doc --workspace --no-deps --open + +.PHONY: bench +bench: + $(CARGO) bench --workspace + +.PHONY: coverage +coverage: + $(CARGO) llvm-cov --workspace --html + @echo "Coverage report: target/llvm-cov/html/index.html" + +.PHONY: update +update: + $(CARGO) update + +.PHONY: audit +audit: + @if command -v cargo-audit >/dev/null 2>&1; then \ + $(CARGO) audit; \ + else \ + echo "cargo-audit not installed. Run: cargo install cargo-audit"; \ + fi + +.PHONY: publish-check +publish-check: + @for crate in $(MEMBERS); do \ + echo "🔍 Memeriksa packaging $$crate"; \ + $(CARGO) package -p "$$crate" || exit 1; \ + done + @echo "✅ Semua crate siap publish." + +.PHONY: publish-all +publish-all: + @for crate in $(PUBLISH_ORDER); do \ + echo "📦 Publishing $$crate ..."; \ + $(CARGO) publish -p $$crate || exit 1; \ + sleep 5; \ + done + @echo "✅ Semua crate berhasil dipublish." + +.PHONY: version +version: + @if [ -z "$(V)" ]; then \ + echo "Usage: make version V="; \ + exit 1; \ + fi + @if [ ! -x "dev/version_bump.sh" ]; then \ + echo "ERROR: dev/version_bump.sh not found or not executable"; \ + exit 1; \ + fi + dev/version_bump.sh $(V) + +.PHONY: snap +snap: + mkdir -p dev + @for crate in $(MEMBERS); do \ + if [ -d "$$crate" ]; then \ + echo "Snapping $$crate/src"; \ + snapcat "$$crate/src" -f markdown -o "dev/$$crate.src.snapcat.md" || true; \ + fi; \ + if [ -d "$$crate/tests" ]; then \ + echo "Snapping $$crate/tests"; \ + snapcat "$$crate/tests" -f markdown -o "dev/$$crate.tests.snapcat.md" || true; \ + fi; \ + done + @echo "Merging all snapshots into dev/root.md" + cat dev/*.snapcat.md > dev/root.md 2>/dev/null || true + @echo "Done. See dev/root.md" + +.PHONY: run +run: + $(CARGO) run -p $(PKG) + +.PHONY: install +install: + $(CARGO) install --path . + +.PHONY: uninstall +uninstall: + $(CARGO) uninstall librawssg || true + +.PHONY: rebuild +rebuild: release install + +.PHONY: build-pkg +build-pkg: + $(CARGO) build -p $(PKG) + +.PHONY: release-pkg +release-pkg: + $(CARGO) build --release -p $(PKG) + +.PHONY: check-pkg +check-pkg: + $(CARGO) check -p $(PKG) + +.PHONY: test-pkg +test-pkg: + $(CARGO) test -p $(PKG) + +.PHONY: test-verbose-pkg +test-verbose-pkg: + RUST_BACKTRACE=1 $(CARGO) test -p $(PKG) -- --nocapture + +.PHONY: fmt-pkg +fmt-pkg: + $(CARGO) fmt -p $(PKG) + +.PHONY: fmt-check-pkg +fmt-check-pkg: + $(CARGO) fmt -p $(PKG) -- --check + +.PHONY: clippy-pkg +clippy-pkg: + $(CARGO) clippy -p $(PKG) --all-targets --all-features $(CLIPPY_FLAGS) + +.PHONY: clippy-pkg-strict +clippy-pkg-strict: + $(CARGO) clippy -p $(PKG) --all-targets --all-features -- -D warnings + +.PHONY: ci-pkg +ci-pkg: fmt-check-pkg clippy-pkg test-verbose-pkg + +.PHONY: ci-pkg-strict +ci-pkg-strict: fmt-check-pkg clippy-pkg-strict test-verbose-pkg + +.PHONY: doc-pkg +doc-pkg: + $(CARGO) doc -p $(PKG) --no-deps + +.PHONY: watch-test-pkg +watch-test-pkg: + $(CARGO) watch -x 'test -p $(PKG)' + +.PHONY: watch-build-pkg +watch-build-pkg: + $(CARGO) watch -x 'check -p $(PKG)' -run: - cargo run +.PHONY: error +error: PKG=librawssg_error +error: ci-pkg -install: - cargo install --path . +.PHONY: fs +fs: PKG=librawssg_fs +fs: ci-pkg -uninstall: - cargo uninstall jsscli +.PHONY: config +config: PKG=librawssg_config +config: ci-pkg -ci: fmt-check clippy test +.PHONY: handler +handler: PKG=librawssg_handler +handler: ci-pkg -rebuild: - make release && make install +.PHONY: templates +templates: PKG=librawssg_templates +templates: ci-pkg -snap: - snapcat tests -f markdown -o dev/tests.snapcat.md && snapcat src -f markdown -o dev/src.snapcat.md \ No newline at end of file +.PHONY: compiler +compiler: PKG=librawssg_compiler +compiler: ci-pkg diff --git a/librawssg_error/tests/integration_tests.rs b/librawssg_error/tests/integration_tests.rs index 8b13789..fa0ff54 100644 --- a/librawssg_error/tests/integration_tests.rs +++ b/librawssg_error/tests/integration_tests.rs @@ -1 +1,33 @@ +use librawssg_error::{Error, Result}; +use std::path::PathBuf; +use thiserror as _; +fn read_file(path: &str) -> Result { + let content = std::fs::read_to_string(path)?; + Ok(content) +} + +#[test] +fn io_error_propagates_via_question_mark() { + let result = read_file("definitely_not_exists.txt"); + assert!(result.is_err()); + assert!(matches!(result, Err(Error::Io(_)))); +} + +#[test] +fn metadata_error_can_hold_boxed_dyn_error() { + let source: Box = + Box::new(std::io::Error::other("bad yaml")); + let err = Error::Metadata { + path: PathBuf::from("content/post.md"), + source, + }; + + assert!(err.to_string().contains("content/post.md")); + let as_core_error: &dyn core::error::Error = &err; + let source_ref = as_core_error.source(); + assert!(source_ref.is_some(), "source should exist"); + if let Some(source_ref) = source_ref { + assert_eq!(source_ref.to_string(), "bad yaml"); + } +} diff --git a/librawssg_error/tests/property_tests.rs b/librawssg_error/tests/property_tests.rs index 8b13789..b78848a 100644 --- a/librawssg_error/tests/property_tests.rs +++ b/librawssg_error/tests/property_tests.rs @@ -1 +1,52 @@ +use librawssg_error::Error; +use thiserror as _; +#[test] +fn config_error_message_preserves_input() { + let samples = [ + "", + "short", + "a very long error message with symbols !@#$%^&*()", + "line1\nline2", + ]; + + for sample in samples { + let err = Error::Config(sample.to_string()); + assert_eq!(err.to_string(), format!("Configuration error: {sample}")); + } +} + +#[test] +fn path_traversal_error_message_preserves_input() { + let samples = [ + "../", + "..\\..\\windows", + "/absolute/path", + "sub/../../escape", + ]; + + for sample in samples { + let err = Error::PathTraversal(sample.to_string()); + assert_eq!( + err.to_string(), + format!("Path traversal attempt detected: {sample}") + ); + } +} + +#[test] +fn render_error_message_preserves_input() { + let samples = [ + "missing variable `title`", + "template not found: base.html", + "unclosed tag", + ]; + + for sample in samples { + let err = Error::Render(sample.to_string()); + assert_eq!( + err.to_string(), + format!("Template rendering error: {sample}") + ); + } +} diff --git a/librawssg_error/tests/unit_tests.rs b/librawssg_error/tests/unit_tests.rs index 8b13789..5ee39a4 100644 --- a/librawssg_error/tests/unit_tests.rs +++ b/librawssg_error/tests/unit_tests.rs @@ -1 +1,156 @@ +use core::error::Error as CoreError; +use librawssg_error::Error; +use std::path::PathBuf; +use thiserror as _; +#[test] +fn display_for_config_error() { + let err = Error::Config("invalid YAML".to_string()); + assert_eq!(err.to_string(), "Configuration error: invalid YAML"); +} + +#[test] +fn display_for_io_error() { + let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing"); + let err = Error::Io(io_err); + assert_eq!(err.to_string(), "I/O error: file missing"); +} + +#[test] +fn display_for_metadata_error() { + let source = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad front matter"); + let err = Error::Metadata { + path: PathBuf::from("content/page.md"), + source: Box::new(source), + }; + assert_eq!( + err.to_string(), + "Failed to parse metadata in content/page.md" + ); +} + +#[test] +fn display_for_render_error() { + let err = Error::Render("template not found".to_string()); + assert_eq!( + err.to_string(), + "Template rendering error: template not found" + ); +} + +#[test] +fn display_for_processor_error() { + let err = Error::Processor("custom processor failed".to_string()); + assert_eq!( + err.to_string(), + "Content processor error: custom processor failed" + ); +} + +#[test] +fn display_for_generator_error() { + let err = Error::Generator("RSS generation failed".to_string()); + assert_eq!(err.to_string(), "Generator error: RSS generation failed"); +} + +#[test] +fn display_for_path_traversal_error() { + let err = Error::PathTraversal("../escape".to_string()); + assert_eq!( + err.to_string(), + "Path traversal attempt detected: ../escape" + ); +} + +#[test] +fn display_for_missing_config_error() { + let err = Error::MissingConfig("base_url".to_string()); + assert_eq!(err.to_string(), "Missing configuration key: base_url"); +} + +#[test] +fn display_for_generation_error() { + let err = Error::Generation("output write failed".to_string()); + assert_eq!( + err.to_string(), + "Site generation error: output write failed" + ); +} + +#[test] +fn display_for_not_found_error() { + let err = Error::NotFound("asset.css".to_string()); + assert_eq!(err.to_string(), "Resource not found: asset.css"); +} + +#[test] +fn display_for_serialization_error() { + let err = Error::Serialization("invalid JSON".to_string()); + assert_eq!(err.to_string(), "Serialization error: invalid JSON"); +} + +#[test] +fn display_for_validation_error() { + let err = Error::Validation("name too long".to_string()); + assert_eq!(err.to_string(), "Validation error: name too long"); +} + +#[test] +fn display_for_duplicate_error() { + let err = Error::Duplicate("duplicate key".to_string()); + assert_eq!(err.to_string(), "Duplicate value: duplicate key"); +} + +#[test] +fn display_for_invalid_state_error() { + let err = Error::InvalidState("unexpected null".to_string()); + assert_eq!(err.to_string(), "Invalid state: unexpected null"); +} + +#[test] +fn display_for_internal_error() { + let err = Error::Internal("bug in code".to_string()); + assert_eq!(err.to_string(), "Internal error: bug in code"); +} + +#[test] +fn implements_std_error_trait() { + let err = Error::Config("test".to_string()); + let as_core_error: &dyn CoreError = &err; + assert!(as_core_error.source().is_none()); +} + +#[test] +fn metadata_source_is_accessible() { + let source = std::io::Error::other("source err"); + let err = Error::Metadata { + path: PathBuf::from("test.txt"), + source: Box::new(source), + }; + let as_core_error: &dyn CoreError = &err; + let source_ref = as_core_error.source(); + assert!(source_ref.is_some(), "source should exist"); + if let Some(source_ref) = source_ref { + assert_eq!(source_ref.to_string(), "source err"); + } +} + +#[test] +fn io_error_from_conversion() { + let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"); + let err: Error = io_err.into(); + assert!(matches!(err, Error::Io(_))); +} + +#[test] +fn result_alias_works() { + let res: librawssg_error::Result<()> = Ok(()); + assert!(res.is_ok()); +} + +#[test] +fn debug_output_contains_variant_name() { + let err = Error::Generator("boom".to_string()); + let debug_str = format!("{err:?}"); + assert!(debug_str.contains("Generator")); +} From a5dfde25d166eb0d79752756f469b67d2fbd7976 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 01:18:25 +0700 Subject: [PATCH 05/20] ci(workflows): run CI on all branches (#28) Change push and pull_request trigger branches from "master" to "*" so CI runs on every branch. This supports the new branching strategy and ensures early feedback for all feature and refactor branches. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30311a7..47ed0c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: ["master"] + branches: ["*"] pull_request: - branches: ["master"] + branches: ["*"] env: CARGO_TERM_COLOR: always From 2726dd735f3d5e5a885a18828032a3453ce02ba7 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 01:39:43 +0700 Subject: [PATCH 06/20] feat(workspace): implement fs and handler core (#29) * chore: update Cargo.lock for new dependencies Add entries for chrono, serde, tempfile, tracing, walkdir and their transitive dependencies. Update thiserror and syn versions to reflect new dependency graph. * feat(fs): add dependencies and dev-dependencies Add tracing and walkdir as runtime dependencies. Add tempfile as a dev dependency for tests. * feat(fs): define FileSystem trait and RealFs export Replace placeholder with a comprehensive FileSystem trait covering file, directory, symlink, and permission operations. Provide default implementations for path safety and fallback copy methods. Export RealFs. * feat(handler): add handler dependencies Add librawssg_error, librawssg_fs, chrono, and serde as dependencies. Chrono is configured without default features, enabling serde support. * feat(handler): expose Document, Metadata, and Processor Replace placeholder with module declarations for document, metadata, and processor. Re-export the public types. * feat(fs): implement RealFs for all FileSystem operations Add a concrete RealFs struct implementing the FileSystem trait. Use std::fs and walkdir to handle I/O, directory traversal, symlinks, permissions, atomic writes, and more. * test(fs): add comprehensive filesystem tests Add a test suite covering read/write, directories, copy, rename, atomic writes, symlinks, permissions, and path safety for the RealFs implementation. * feat(handler): define Document structure Add Document struct with metadata, body, URL, source path, depth, content type, list flag, and optional child documents. * feat(handler): define Metadata struct Add Metadata struct with title, description, author, repo URL, license, date, tags, and draft flag. Derives Serialize, Deserialize, and Default. * feat(handler): define Processor trait Add Processor trait with can_process and process methods. The process method accepts FileSystem and paths, returning Result>. --- Cargo.lock | 248 +++++++++++++- librawssg_fs/Cargo.toml | 5 + librawssg_fs/src/lib.rs | 124 ++++++- librawssg_fs/src/real.rs | 165 +++++++++ librawssg_fs/tests/filesystem.rs | 527 +++++++++++++++++++++++++++++ librawssg_handler/Cargo.toml | 4 + librawssg_handler/src/document.rs | 14 + librawssg_handler/src/lib.rs | 19 +- librawssg_handler/src/metadata.rs | 14 + librawssg_handler/src/processor.rs | 15 + 10 files changed, 1111 insertions(+), 24 deletions(-) create mode 100644 librawssg_fs/src/real.rs create mode 100644 librawssg_fs/tests/filesystem.rs create mode 100644 librawssg_handler/src/document.rs create mode 100644 librawssg_handler/src/metadata.rs create mode 100644 librawssg_handler/src/processor.rs diff --git a/Cargo.lock b/Cargo.lock index 84f6ee0..8734180 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,67 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + [[package]] name = "librawssg_compiler" version = "1.0.0" @@ -20,15 +81,53 @@ dependencies = [ [[package]] name = "librawssg_fs" version = "1.0.0" +dependencies = [ + "tempfile", + "tracing", + "walkdir", +] [[package]] name = "librawssg_handler" version = "1.0.0" +dependencies = [ + "chrono", + "librawssg_error", + "librawssg_fs", + "serde", +] [[package]] name = "librawssg_templates" version = "1.0.0" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -47,6 +146,75 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.5" @@ -58,6 +226,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "2.0.20" @@ -75,7 +256,38 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", ] [[package]] @@ -83,3 +295,37 @@ name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/librawssg_fs/Cargo.toml b/librawssg_fs/Cargo.toml index 05b1145..f9beb47 100644 --- a/librawssg_fs/Cargo.toml +++ b/librawssg_fs/Cargo.toml @@ -10,6 +10,11 @@ keywords = ["ssg", "static-site-generator", "filesystem", "io"] categories = ["development-tools::build-utils"] [dependencies] +tracing = "0.1.44" +walkdir = "2.5.0" [lints] workspace = true + +[dev-dependencies] +tempfile = "3.27.0" diff --git a/librawssg_fs/src/lib.rs b/librawssg_fs/src/lib.rs index b93cf3f..69040cf 100644 --- a/librawssg_fs/src/lib.rs +++ b/librawssg_fs/src/lib.rs @@ -1,14 +1,118 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub mod real; -#[cfg(test)] -mod tests { - use super::*; +use std::io; +use std::path::{Path, PathBuf}; + +use tracing as _; +use walkdir as _; + +pub trait FileSystem: Send + Sync { + fn read_to_string(&self, path: &Path) -> io::Result; + fn read_bytes(&self, path: &Path) -> io::Result>; + fn write(&self, path: &Path, content: &[u8]) -> io::Result<()>; + fn create_dir_all(&self, path: &Path) -> io::Result<()>; + fn remove_dir_all(&self, path: &Path) -> io::Result<()>; + fn remove_file(&self, path: &Path) -> io::Result<()>; + fn create_dir(&self, path: &Path) -> io::Result<()>; + fn exists(&self, path: &Path) -> bool; + fn is_dir(&self, path: &Path) -> bool; + fn is_file(&self, path: &Path) -> bool; + fn read_dir(&self, path: &Path) -> io::Result>; + fn copy_file(&self, from: &Path, to: &Path) -> io::Result; + fn copy_dir_all(&self, from: &Path, to: &Path) -> io::Result<()>; + fn walk_dir(&self, root: &Path) -> io::Result>; + fn canonicalize(&self, path: &Path) -> io::Result; + fn rename(&self, from: &Path, to: &Path) -> io::Result<()>; + fn atomic_write(&self, path: &Path, content: &[u8]) -> io::Result<()>; + fn touch(&self, path: &Path) -> io::Result<()>; + fn metadata(&self, path: &Path) -> io::Result; + fn symlink_metadata(&self, path: &Path) -> io::Result; + fn permissions(&self, path: &Path) -> io::Result; + fn set_permissions(&self, path: &Path, permissions: std::fs::Permissions) -> io::Result<()>; + fn read_link(&self, path: &Path) -> io::Result; + fn hard_link(&self, from: &Path, to: &Path) -> io::Result<()>; + + fn is_symlink(&self, path: &Path) -> bool { + self.symlink_metadata(path) + .is_ok_and(|meta| meta.file_type().is_symlink()) + } + + fn canonicalize_or_join(&self, base: &Path, candidate: &Path) -> io::Result { + let mut components: Vec> = Vec::new(); + for comp in candidate.components() { + match comp { + std::path::Component::CurDir => {} + std::path::Component::ParentDir => { + if components.is_empty() { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "path traversal detected", + )); + } + if matches!(components.last(), Some(std::path::Component::Normal(_))) { + let _ = components.pop(); + } else { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "path traversal detected", + )); + } + } + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::Normal(_) => components.push(comp), + } + } + let normalized_candidate: PathBuf = components.into_iter().collect(); + let joined = base.join(normalized_candidate); + + if self.exists(&joined) { + self.canonicalize(&joined) + } else { + let parent = joined + .parent() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no parent"))?; + let parent_canon = self.canonicalize(parent)?; + let file_name = joined.file_name().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "path has no file name") + })?; + Ok(parent_canon.join(file_name)) + } + } + + fn safe_join(&self, base: &Path, candidate: &Path) -> io::Result { + let base_canon = self.canonicalize(base)?; + let joined = self.canonicalize_or_join(&base_canon, candidate)?; + if !joined.starts_with(&base_canon) { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "path traversal detected", + )); + } + Ok(joined) + } + + fn copy(&self, from: &Path, to: &Path) -> io::Result<()> { + if self.is_dir(from) { + self.copy_dir_all(from, to) + } else { + self.copy_file(from, to).map(|_| ()) + } + } - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn rename_or_copy(&self, from: &Path, to: &Path) -> io::Result<()> { + match self.rename(from, to) { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::CrossesDevices => { + self.copy_dir_all(from, to)?; + self.remove_dir_all(from) + } + Err(e) => Err(e), + } } } + +pub use real::RealFs; + +#[cfg(test)] +use tempfile as _; diff --git a/librawssg_fs/src/real.rs b/librawssg_fs/src/real.rs new file mode 100644 index 0000000..8d73142 --- /dev/null +++ b/librawssg_fs/src/real.rs @@ -0,0 +1,165 @@ +use super::FileSystem; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +#[derive(Debug, Default, Clone, Copy)] +pub struct RealFs; + +impl FileSystem for RealFs { + #[tracing::instrument(skip(self))] + fn read_to_string(&self, path: &Path) -> io::Result { + fs::read_to_string(path) + } + + #[tracing::instrument(skip(self))] + fn read_bytes(&self, path: &Path) -> io::Result> { + fs::read(path) + } + + #[tracing::instrument(skip(self, content))] + fn write(&self, path: &Path, content: &[u8]) -> io::Result<()> { + if let Some(parent) = path.parent() { + self.create_dir_all(parent)?; + } + fs::write(path, content) + } + + #[tracing::instrument(skip(self))] + fn create_dir_all(&self, path: &Path) -> io::Result<()> { + fs::create_dir_all(path) + } + + #[tracing::instrument(skip(self))] + fn remove_dir_all(&self, path: &Path) -> io::Result<()> { + fs::remove_dir_all(path) + } + + #[tracing::instrument(skip(self))] + fn remove_file(&self, path: &Path) -> io::Result<()> { + fs::remove_file(path) + } + + #[tracing::instrument(skip(self))] + fn create_dir(&self, path: &Path) -> io::Result<()> { + fs::create_dir(path) + } + + #[tracing::instrument(skip(self))] + fn exists(&self, path: &Path) -> bool { + path.exists() + } + + #[tracing::instrument(skip(self))] + fn is_dir(&self, path: &Path) -> bool { + path.is_dir() + } + + #[tracing::instrument(skip(self))] + fn is_file(&self, path: &Path) -> bool { + path.is_file() + } + + #[tracing::instrument(skip(self))] + fn read_dir(&self, path: &Path) -> io::Result> { + let mut entries = Vec::new(); + for entry in fs::read_dir(path)? { + entries.push(entry?.path()); + } + Ok(entries) + } + + #[tracing::instrument(skip(self))] + fn copy_file(&self, from: &Path, to: &Path) -> io::Result { + fs::copy(from, to) + } + + #[tracing::instrument(skip(self))] + fn copy_dir_all(&self, from: &Path, to: &Path) -> io::Result<()> { + self.create_dir_all(to)?; + for entry in self.walk_dir(from)? { + let rel = entry + .strip_prefix(from) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + let dest = to.join(rel); + if self.is_dir(&entry) { + self.create_dir_all(&dest)?; + } else { + if let Some(parent) = dest.parent() { + self.create_dir_all(parent)?; + } + let _ = self.copy_file(&entry, &dest)?; + } + } + Ok(()) + } + + #[tracing::instrument(skip(self))] + fn walk_dir(&self, root: &Path) -> io::Result> { + let mut files = Vec::new(); + for entry in WalkDir::new(root) { + let entry = entry?; + if entry.file_type().is_file() { + files.push(entry.into_path()); + } + } + Ok(files) + } + + fn canonicalize(&self, path: &Path) -> io::Result { + path.canonicalize() + } + + fn rename(&self, from: &Path, to: &Path) -> io::Result<()> { + fs::rename(from, to) + } + + fn atomic_write(&self, path: &Path, content: &[u8]) -> io::Result<()> { + let tmp = path.with_extension("tmp"); + self.write(&tmp, content)?; + match self.rename(&tmp, path) { + Ok(()) => Ok(()), + Err(e) => { + let _ = self.remove_file(&tmp); + Err(e) + } + } + } + + fn touch(&self, path: &Path) -> io::Result<()> { + if let Some(parent) = path.parent() { + self.create_dir_all(parent)?; + } + let file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(path)?; + file.sync_all()?; + Ok(()) + } + + fn metadata(&self, path: &Path) -> io::Result { + fs::metadata(path) + } + + fn symlink_metadata(&self, path: &Path) -> io::Result { + fs::symlink_metadata(path) + } + + fn permissions(&self, path: &Path) -> io::Result { + fs::metadata(path).map(|m| m.permissions()) + } + + fn set_permissions(&self, path: &Path, permissions: fs::Permissions) -> io::Result<()> { + fs::set_permissions(path, permissions) + } + + fn read_link(&self, path: &Path) -> io::Result { + fs::read_link(path) + } + + fn hard_link(&self, from: &Path, to: &Path) -> io::Result<()> { + fs::hard_link(from, to) + } +} diff --git a/librawssg_fs/tests/filesystem.rs b/librawssg_fs/tests/filesystem.rs new file mode 100644 index 0000000..561ad4b --- /dev/null +++ b/librawssg_fs/tests/filesystem.rs @@ -0,0 +1,527 @@ +use librawssg_fs::{FileSystem, RealFs}; +use std::path::Path; +use tempfile::TempDir; +use tracing as _; +use walkdir as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +#[test] +fn write_and_read_string() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file_path = tmp.path().join("hello.txt"); + + must!(fs.write(&file_path, b"world"), "write"); + let content = must!(fs.read_to_string(&file_path), "read_to_string"); + assert_eq!(content, "world"); +} + +#[test] +fn write_and_read_bytes() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file_path = tmp.path().join("data.bin"); + let data = vec![0, 1, 2, 3]; + + must!(fs.write(&file_path, &data), "write"); + let read = must!(fs.read_bytes(&file_path), "read_bytes"); + assert_eq!(read, data); +} + +#[test] +fn read_nonexistent_file_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.read_to_string(&tmp.path().join("missing.txt")); + assert!(result.is_err()); +} + +#[test] +fn write_creates_parent_directories() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let nested = tmp.path().join("a/b/c/file.txt"); + + must!(fs.write(&nested, b"deep"), "write nested"); + assert!(fs.exists(&nested)); + let content = must!(fs.read_to_string(&nested), "read nested"); + assert_eq!(content, "deep"); +} + +#[test] +fn create_dir_and_check_exists() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let dir_path = tmp.path().join("subdir"); + + must!(fs.create_dir(&dir_path), "create_dir"); + assert!(fs.exists(&dir_path)); + assert!(fs.is_dir(&dir_path)); + assert!(!fs.is_file(&dir_path)); +} + +#[test] +fn create_dir_already_exists_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let dir = tmp.path().join("existing_dir"); + must!(fs.create_dir(&dir), "create_dir first"); + + let result = fs.create_dir(&dir); + assert!(result.is_err(), "creating existing dir should error"); +} + +#[test] +fn create_dir_all_recursive() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let nested = tmp.path().join("a/b/c"); + + must!(fs.create_dir_all(&nested), "create_dir_all"); + assert!(fs.exists(&nested)); + assert!(fs.is_dir(&nested)); +} + +#[test] +fn read_dir_lists_entries() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let dir = tmp.path().join("dir"); + must!(fs.create_dir_all(&dir), "create_dir_all"); + must!(fs.write(&dir.join("a.txt"), b"a"), "write a"); + must!(fs.write(&dir.join("b.txt"), b"b"), "write b"); + + let entries = must!(fs.read_dir(&dir), "read_dir"); + assert_eq!(entries.len(), 2); + assert!(entries.iter().any(|p| p.ends_with("a.txt"))); + assert!(entries.iter().any(|p| p.ends_with("b.txt"))); +} + +#[test] +fn read_dir_empty_returns_empty_vec() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let dir = tmp.path().join("empty"); + must!(fs.create_dir(&dir), "create_dir"); + + let entries = must!(fs.read_dir(&dir), "read_dir"); + assert!(entries.is_empty()); +} + +#[test] +fn remove_dir_all_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let dir = tmp.path().join("dir"); + must!(fs.create_dir_all(&dir.join("sub")), "create_dir_all"); + must!(fs.write(&dir.join("file.txt"), b"data"), "write"); + + must!(fs.remove_dir_all(&dir), "remove_dir_all"); + assert!(!fs.exists(&dir)); +} + +#[test] +fn remove_dir_all_nonexistent_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.remove_dir_all(&tmp.path().join("ghost")); + assert!(result.is_err()); +} + +#[test] +fn copy_file_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let src = tmp.path().join("src.txt"); + let dst = tmp.path().join("dst.txt"); + must!(fs.write(&src, b"data"), "write"); + + let copied = must!(fs.copy_file(&src, &dst), "copy_file"); + assert!(copied > 0); + let content = must!(fs.read_to_string(&dst), "read dst"); + assert_eq!(content, "data"); +} + +#[test] +fn copy_file_nonexistent_source_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.copy_file(&tmp.path().join("missing"), &tmp.path().join("dest")); + assert!(result.is_err()); +} + +#[test] +fn rename_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let old = tmp.path().join("old.txt"); + let new = tmp.path().join("new.txt"); + must!(fs.write(&old, b"content"), "write"); + + must!(fs.rename(&old, &new), "rename"); + assert!(!fs.exists(&old)); + assert!(fs.exists(&new)); + let content = must!(fs.read_to_string(&new), "read new"); + assert_eq!(content, "content"); +} + +#[test] +fn rename_nonexistent_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.rename(&tmp.path().join("missing"), &tmp.path().join("dest")); + assert!(result.is_err()); +} + +#[test] +fn remove_file_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("delete.txt"); + must!(fs.write(&file, b"to delete"), "write"); + + must!(fs.remove_file(&file), "remove_file"); + assert!(!fs.exists(&file)); +} + +#[test] +fn remove_file_nonexistent_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.remove_file(&tmp.path().join("missing.txt")); + assert!(result.is_err()); +} + +#[test] +fn atomic_write_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("atomic.txt"); + + must!(fs.atomic_write(&file, b"first"), "atomic_write first"); + let content = must!(fs.read_to_string(&file), "read first"); + assert_eq!(content, "first"); + + must!(fs.atomic_write(&file, b"second"), "atomic_write second"); + let content = must!(fs.read_to_string(&file), "read second"); + assert_eq!(content, "second"); +} + +#[test] +fn atomic_write_creates_parents() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("parent/child/atomic.txt"); + + must!(fs.atomic_write(&file, b"nested"), "atomic_write nested"); + assert!(fs.exists(&file)); + let content = must!(fs.read_to_string(&file), "read nested"); + assert_eq!(content, "nested"); +} + +#[test] +fn touch_creates_file() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("new_file.txt"); + + must!(fs.touch(&file), "touch"); + assert!(fs.exists(&file)); + let content = must!(fs.read_to_string(&file), "read touched"); + assert_eq!(content, ""); +} + +#[test] +fn touch_existing_file_keeps_content() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("existing.txt"); + must!(fs.write(&file, b"keep"), "write"); + + must!(fs.touch(&file), "touch"); + let content = must!(fs.read_to_string(&file), "read after touch"); + assert_eq!(content, "keep"); +} + +#[test] +fn copy_dir_all_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let src_dir = tmp.path().join("src_dir"); + let dst_dir = tmp.path().join("dst_dir"); + must!(fs.create_dir_all(&src_dir.join("nested")), "create_dir_all"); + must!(fs.write(&src_dir.join("file1.txt"), b"one"), "write file1"); + must!( + fs.write(&src_dir.join("nested").join("file2.txt"), b"two"), + "write file2" + ); + + must!(fs.copy_dir_all(&src_dir, &dst_dir), "copy_dir_all"); + assert!(fs.exists(&dst_dir.join("file1.txt"))); + assert!(fs.exists(&dst_dir.join("nested").join("file2.txt"))); + let content = must!(fs.read_to_string(&dst_dir.join("file1.txt")), "read file1"); + assert_eq!(content, "one"); +} + +#[test] +fn copy_file_via_copy_method() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let src = tmp.path().join("src.txt"); + let dst = tmp.path().join("dst.txt"); + must!(fs.write(&src, b"copy me"), "write src"); + + must!(fs.copy(&src, &dst), "copy file"); + assert!(fs.exists(&dst)); + let content = must!(fs.read_to_string(&dst), "read dst"); + assert_eq!(content, "copy me"); +} + +#[test] +fn copy_dir_via_copy_method() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let src = tmp.path().join("src_dir"); + let dst = tmp.path().join("dst_dir"); + must!(fs.create_dir_all(&src), "create_dir_all"); + must!(fs.write(&src.join("file.txt"), b"dir copy"), "write file"); + + must!(fs.copy(&src, &dst), "copy dir"); + assert!(fs.exists(&dst.join("file.txt"))); + let content = must!(fs.read_to_string(&dst.join("file.txt")), "read dst file"); + assert_eq!(content, "dir copy"); +} + +#[test] +fn walk_dir_collects_files_recursively() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let root = tmp.path().join("root"); + must!(fs.create_dir_all(&root.join("sub")), "create_dir_all"); + must!(fs.write(&root.join("root.txt"), b"root"), "write root"); + must!( + fs.write(&root.join("sub").join("sub.txt"), b"sub"), + "write sub" + ); + + let files = must!(fs.walk_dir(&root), "walk_dir"); + assert_eq!(files.len(), 2); + assert!(files.iter().any(|p| p.ends_with("root.txt"))); + assert!(files.iter().any(|p| p.ends_with("sub.txt"))); +} + +#[test] +fn walk_dir_empty_directory_returns_empty() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let dir = tmp.path().join("empty"); + must!(fs.create_dir(&dir), "create_dir"); + + let files = must!(fs.walk_dir(&dir), "walk_dir"); + assert!(files.is_empty()); +} + +#[test] +fn canonicalize_or_join_works_for_existing_and_missing() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let base = tmp.path(); + let existing = base.join("existing.txt"); + must!(fs.write(&existing, b"data"), "write"); + + let canon_existing = must!( + fs.canonicalize_or_join(base, Path::new("existing.txt")), + "canonicalize existing" + ); + let canon_direct = must!(fs.canonicalize(&existing), "canonicalize direct"); + assert_eq!(canon_existing, canon_direct); + + let missing = Path::new("missing.txt"); + let canon_missing = must!( + fs.canonicalize_or_join(base, missing), + "canonicalize missing" + ); + let canon_base = must!(fs.canonicalize(base), "canonicalize base"); + assert_eq!(canon_missing, canon_base.join(missing)); +} + +#[test] +fn safe_join_blocks_traversal() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let base = tmp.path().join("base"); + must!(fs.create_dir_all(&base), "create base"); + + let safe = must!( + fs.safe_join(&base, Path::new("inside.txt")), + "safe join inside" + ); + assert!(safe.starts_with(&base)); + + let traversal = Path::new("../escape.txt"); + let result = fs.safe_join(&base, traversal); + assert!(result.is_err(), "traversal should be rejected"); +} + +#[test] +fn safe_join_allows_dot_segments_inside() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let base = tmp.path().join("base"); + must!(fs.create_dir_all(&base), "create base"); + + let candidate = Path::new("sub/../file.txt"); + let result = must!( + fs.safe_join(&base, candidate), + "safe join with dot segments" + ); + let expected = must!(fs.canonicalize(&base), "canonicalize base").join("file.txt"); + assert_eq!(result, expected); +} + +#[test] +fn metadata_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("meta.txt"); + must!(fs.write(&file, b"hello"), "write"); + + let meta = must!(fs.metadata(&file), "metadata"); + assert_eq!(meta.len(), 5); + assert!(meta.is_file()); +} + +#[test] +fn metadata_nonexistent_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.metadata(&tmp.path().join("missing.txt")); + assert!(result.is_err()); +} + +#[test] +fn symlink_metadata_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("file.txt"); + must!(fs.write(&file, b"data"), "write"); + + let meta = must!(fs.symlink_metadata(&file), "symlink_metadata"); + assert!(meta.is_file()); +} + +#[test] +fn permissions_roundtrip() { + use std::os::unix::fs::PermissionsExt; + + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("perm.txt"); + must!(fs.write(&file, b"data"), "write"); + + let mut perms = must!(fs.permissions(&file), "permissions"); + perms.set_mode(0o600); + must!(fs.set_permissions(&file, perms.clone()), "set_permissions"); + + let read_perms = must!(fs.permissions(&file), "permissions after set"); + assert_eq!(read_perms.mode() & 0o777, 0o600); +} + +#[test] +fn permissions_nonexistent_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.permissions(&tmp.path().join("missing.txt")); + assert!(result.is_err()); +} +#[test] +fn set_permissions_nonexistent_errors() { + use std::os::unix::fs::PermissionsExt; + + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let perms = std::fs::Permissions::from_mode(0o644); + let result = fs.set_permissions(&tmp.path().join("missing.txt"), perms); + assert!(result.is_err()); +} + +#[cfg(unix)] +#[test] +fn symlink_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let target = tmp.path().join("target.txt"); + let link = tmp.path().join("link.txt"); + must!(fs.write(&target, b"target"), "write target"); + + must!(std::os::unix::fs::symlink(&target, &link), "symlink"); + assert!(fs.is_symlink(&link)); + let link_target = must!(fs.read_link(&link), "read_link"); + assert_eq!(link_target, target); + let content = must!(fs.read_to_string(&link), "read through symlink"); + assert_eq!(content, "target"); +} + +#[cfg(unix)] +#[test] +fn is_symlink_false_for_regular_file() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let file = tmp.path().join("regular.txt"); + must!(fs.write(&file, b"data"), "write"); + assert!(!fs.is_symlink(&file)); +} + +#[cfg(unix)] +#[test] +fn hard_link_works() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let original = tmp.path().join("original.txt"); + let hard = tmp.path().join("hard.txt"); + must!(fs.write(&original, b"shared"), "write original"); + + must!(fs.hard_link(&original, &hard), "hard_link"); + assert!(fs.exists(&hard)); + let content = must!(fs.read_to_string(&hard), "read hard"); + assert_eq!(content, "shared"); +} + +#[cfg(unix)] +#[test] +fn hard_link_nonexistent_source_errors() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let result = fs.hard_link( + &tmp.path().join("missing.txt"), + &tmp.path().join("hard.txt"), + ); + assert!(result.is_err()); +} + +#[test] +fn rename_or_copy_fallback_renames_on_same_device() { + let tmp = must!(TempDir::new(), "TempDir::new"); + let fs = RealFs; + let src = tmp.path().join("src_dir"); + let dst = tmp.path().join("dst_dir"); + must!(fs.create_dir_all(&src), "create src"); + must!(fs.write(&src.join("file.txt"), b"data"), "write file"); + + must!(fs.rename_or_copy(&src, &dst), "rename_or_copy"); + assert!(!fs.exists(&src)); + assert!(fs.exists(&dst)); + let content = must!(fs.read_to_string(&dst.join("file.txt")), "read dst"); + assert_eq!(content, "data"); +} diff --git a/librawssg_handler/Cargo.toml b/librawssg_handler/Cargo.toml index c4700d7..3ed2e16 100644 --- a/librawssg_handler/Cargo.toml +++ b/librawssg_handler/Cargo.toml @@ -10,6 +10,10 @@ keywords = ["ssg", "static-site-generator", "content", "processor", "traits"] categories = ["development-tools::build-utils"] [dependencies] +librawssg_error = { path = "../librawssg_error" } +librawssg_fs = { path = "../librawssg_fs" } +chrono = { version = "0.4", default-features = false, features = ["serde"] } +serde = { version = "1.0.229", features = ["derive"] } [lints] workspace = true diff --git a/librawssg_handler/src/document.rs b/librawssg_handler/src/document.rs new file mode 100644 index 0000000..0bba99f --- /dev/null +++ b/librawssg_handler/src/document.rs @@ -0,0 +1,14 @@ +use crate::Metadata; +use std::path::PathBuf; + +#[derive(Debug, Clone, PartialEq)] +pub struct Document { + pub metadata: Metadata, + pub body: String, + pub url: String, + pub source_path: PathBuf, + pub depth: usize, + pub content_type: String, + pub is_list: bool, + pub list_items: Option>, +} diff --git a/librawssg_handler/src/lib.rs b/librawssg_handler/src/lib.rs index b93cf3f..ed443b7 100644 --- a/librawssg_handler/src/lib.rs +++ b/librawssg_handler/src/lib.rs @@ -1,14 +1,7 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub mod document; +pub mod metadata; +pub mod processor; -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use document::Document; +pub use metadata::Metadata; +pub use processor::Processor; diff --git a/librawssg_handler/src/metadata.rs b/librawssg_handler/src/metadata.rs new file mode 100644 index 0000000..e3f31bc --- /dev/null +++ b/librawssg_handler/src/metadata.rs @@ -0,0 +1,14 @@ +use chrono::NaiveDate; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Metadata { + pub title: String, + pub description: String, + pub author: Option, + pub repo_url: Option, + pub license: Option, + pub date: Option, + pub tags: Vec, + pub draft: bool, +} diff --git a/librawssg_handler/src/processor.rs b/librawssg_handler/src/processor.rs new file mode 100644 index 0000000..e6b7e2c --- /dev/null +++ b/librawssg_handler/src/processor.rs @@ -0,0 +1,15 @@ +use crate::Document; +use librawssg_error::Result; +use librawssg_fs::FileSystem; +use std::path::Path; + +pub trait Processor: Send + Sync { + fn can_process(&self, relative_path: &Path, original_path: &Path) -> bool; + + fn process( + &self, + fs: &dyn FileSystem, + relative_path: &Path, + content_dir: &Path, + ) -> Result>; +} From 87806dcd1c617f00c1fbbfd95ed6b4a50e747336 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 01:50:23 +0700 Subject: [PATCH 07/20] feat(handler): add core data structures and processor extensions (#30) * chore(handler): update lockfile for new dependencies Add serde_json and its transitive dependencies to Cargo.lock. This reflects the new serde_json dependency in librawssg_handler and keeps the lockfile in sync with Cargo.toml. * feat(handler): add serde_json dependency Add serde_json version 1.0.151 to librawssg_handler dependencies. This crate will be used for storing and manipulating extra metadata as JSON values. * feat(handler): enhance Document with validation and taxonomies Add `output_path`, `taxonomies`, and derive `PartialEq`. Make the struct non-exhaustive. Implement a constructor `new` with validation for URL, output_path, source_path, and depth. Add helper methods `relative_url`, `add_taxonomy`, and `depth`. * refactor(handler): allow multiple crate versions lint Add `#![allow(clippy::multiple_crate_versions)]` at crate root to suppress warnings about multiple versions of dependencies across the workspace. This keeps clippy output clean without affecting functionality. * feat(handler): extend Metadata with extra fields and methods Add `updated: Option` and `extra: HashMap` fields. Make the struct non-exhaustive. Implement a constructor `new` that validates a non-empty title. Add helper methods `is_draft`, `insert_extra`, and `get_extra`. * feat(handler): add name and priority to Processor trait Extend the `Processor` trait with a required `name()` method and a default `priority()` method. These methods allow processors to be identified and ordered during content processing. * test(handler): add tests for Document Add comprehensive tests covering Document construction, validation errors, relative_url, add_taxonomy, depth, and default fields. * test(handler): add tests for Metadata Add tests for Metadata construction, validation, extra fields, draft flag, serialization, and deserialization from JSON. * test(handler): add tests for Processor trait Add tests using a mock Processor to verify name, priority, can_process, and process behavior. Includes a test for error propagation. * test(handler): add basic reexport availability test Add a test that ensures reexports of Metadata, Document, and Processor are available. This catches missing or broken public exports. --- Cargo.lock | 32 +++ librawssg_handler/Cargo.toml | 1 + librawssg_handler/src/document.rs | 69 +++++- librawssg_handler/src/lib.rs | 2 + librawssg_handler/src/metadata.rs | 37 +++ librawssg_handler/src/processor.rs | 6 + librawssg_handler/tests/document_tests.rs | 163 +++++++++++++ librawssg_handler/tests/metadata_tests.rs | 128 ++++++++++ librawssg_handler/tests/processor_tests.rs | 259 +++++++++++++++++++++ librawssg_handler/tests/unit_tests.rs | 20 ++ 10 files changed, 716 insertions(+), 1 deletion(-) create mode 100644 librawssg_handler/tests/document_tests.rs create mode 100644 librawssg_handler/tests/metadata_tests.rs create mode 100644 librawssg_handler/tests/processor_tests.rs create mode 100644 librawssg_handler/tests/unit_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 8734180..315859d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,12 @@ dependencies = [ "r-efi", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libc" version = "0.2.189" @@ -95,6 +101,7 @@ dependencies = [ "librawssg_error", "librawssg_fs", "serde", + "serde_json", ] [[package]] @@ -107,6 +114,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "num-traits" version = "0.2.19" @@ -204,6 +217,19 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "syn" version = "2.0.119" @@ -329,3 +355,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/librawssg_handler/Cargo.toml b/librawssg_handler/Cargo.toml index 3ed2e16..829e237 100644 --- a/librawssg_handler/Cargo.toml +++ b/librawssg_handler/Cargo.toml @@ -14,6 +14,7 @@ librawssg_error = { path = "../librawssg_error" } librawssg_fs = { path = "../librawssg_fs" } chrono = { version = "0.4", default-features = false, features = ["serde"] } serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" [lints] workspace = true diff --git a/librawssg_handler/src/document.rs b/librawssg_handler/src/document.rs index 0bba99f..b9cad17 100644 --- a/librawssg_handler/src/document.rs +++ b/librawssg_handler/src/document.rs @@ -1,14 +1,81 @@ use crate::Metadata; +use librawssg_error::{Error, Result}; +use std::collections::HashMap; use std::path::PathBuf; #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub struct Document { pub metadata: Metadata, pub body: String, pub url: String, + pub output_path: PathBuf, pub source_path: PathBuf, pub depth: usize, pub content_type: String, pub is_list: bool, - pub list_items: Option>, + pub list_items: Option>, + pub taxonomies: HashMap>, +} + +impl Document { + #[allow(clippy::too_many_arguments)] + pub fn new( + metadata: Metadata, + body: impl Into, + url: impl Into, + output_path: impl Into, + source_path: impl Into, + depth: usize, + content_type: impl Into, + is_list: bool, + ) -> Result { + let url = url.into(); + if url.trim().is_empty() { + return Err(Error::Validation("document url cannot be empty".into())); + } + let output_path = output_path.into(); + if output_path.as_os_str().is_empty() { + return Err(Error::Validation( + "document output_path cannot be empty".into(), + )); + } + let source_path = source_path.into(); + if source_path.file_name().is_none() { + return Err(Error::Validation( + "document source_path must have a file name".into(), + )); + } + if depth > 1000 { + return Err(Error::Validation( + "document depth is unreasonably large".into(), + )); + } + Ok(Self { + metadata, + body: body.into(), + url, + output_path, + source_path, + depth, + content_type: content_type.into(), + is_list, + list_items: None, + taxonomies: HashMap::new(), + }) + } + + #[must_use] + pub fn relative_url(&self) -> &str { + &self.url + } + + pub fn add_taxonomy(&mut self, name: impl Into, items: Vec) { + let _ = self.taxonomies.insert(name.into(), items); + } + + #[must_use] + pub const fn depth(&self) -> usize { + self.depth + } } diff --git a/librawssg_handler/src/lib.rs b/librawssg_handler/src/lib.rs index ed443b7..6751f36 100644 --- a/librawssg_handler/src/lib.rs +++ b/librawssg_handler/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::multiple_crate_versions)] + pub mod document; pub mod metadata; pub mod processor; diff --git a/librawssg_handler/src/metadata.rs b/librawssg_handler/src/metadata.rs index e3f31bc..7d6b60e 100644 --- a/librawssg_handler/src/metadata.rs +++ b/librawssg_handler/src/metadata.rs @@ -1,7 +1,9 @@ use chrono::NaiveDate; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[non_exhaustive] pub struct Metadata { pub title: String, pub description: String, @@ -9,6 +11,41 @@ pub struct Metadata { pub repo_url: Option, pub license: Option, pub date: Option, + pub updated: Option, pub tags: Vec, pub draft: bool, + pub extra: HashMap, +} + +impl Metadata { + pub fn new( + title: impl Into, + description: impl Into, + ) -> librawssg_error::Result { + let title = title.into(); + if title.trim().is_empty() { + return Err(librawssg_error::Error::Validation( + "metadata title cannot be empty".into(), + )); + } + Ok(Self { + title, + description: description.into(), + ..Default::default() + }) + } + + #[must_use] + pub const fn is_draft(&self) -> bool { + self.draft + } + + pub fn insert_extra(&mut self, key: impl Into, value: impl Into) { + let _ = self.extra.insert(key.into(), value.into()); + } + + #[must_use] + pub fn get_extra(&self, key: &str) -> Option<&serde_json::Value> { + self.extra.get(key) + } } diff --git a/librawssg_handler/src/processor.rs b/librawssg_handler/src/processor.rs index e6b7e2c..5ff7b2d 100644 --- a/librawssg_handler/src/processor.rs +++ b/librawssg_handler/src/processor.rs @@ -4,6 +4,12 @@ use librawssg_fs::FileSystem; use std::path::Path; pub trait Processor: Send + Sync { + fn name(&self) -> &str; + + fn priority(&self) -> i32 { + 0 + } + fn can_process(&self, relative_path: &Path, original_path: &Path) -> bool; fn process( diff --git a/librawssg_handler/tests/document_tests.rs b/librawssg_handler/tests/document_tests.rs new file mode 100644 index 0000000..9d4a38c --- /dev/null +++ b/librawssg_handler/tests/document_tests.rs @@ -0,0 +1,163 @@ +use chrono as _; +use librawssg_fs as _; +use librawssg_handler::{Document, Metadata}; +use serde as _; +use serde_json as _; +use std::path::PathBuf; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +fn valid_metadata() -> Metadata { + must!(Metadata::new("Title", "Description"), "Metadata::new") +} + +fn valid_document() -> Document { + must!( + Document::new( + valid_metadata(), + "

Body

", + "blog/my-post.html", + "blog/my-post/index.html", + "content/blog/my-post.md", + 1, + "blog", + false, + ), + "Document::new" + ) +} + +#[test] +fn new_valid_document_ok() { + let doc = valid_document(); + assert_eq!(doc.metadata.title, "Title"); + assert_eq!(doc.body, "

Body

"); + assert_eq!(doc.url, "blog/my-post.html"); + assert_eq!(doc.output_path, PathBuf::from("blog/my-post/index.html")); + assert_eq!(doc.source_path, PathBuf::from("content/blog/my-post.md")); + assert_eq!(doc.depth, 1); + assert_eq!(doc.content_type, "blog"); + assert!(!doc.is_list); + assert!(doc.list_items.is_none()); + assert!(doc.taxonomies.is_empty()); +} + +#[test] +fn new_empty_url_errors() { + let result = Document::new( + valid_metadata(), + "body", + "", + "out", + "src.md", + 0, + "page", + false, + ); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!( + err, + librawssg_error::Error::Validation(ref msg) if msg == "document url cannot be empty" + )); + } +} + +#[test] +fn new_empty_output_path_errors() { + let result = Document::new( + valid_metadata(), + "body", + "url", + "", + "src.md", + 0, + "page", + false, + ); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!( + err, + librawssg_error::Error::Validation(ref msg) if msg == "document output_path cannot be empty" + )); + } +} + +#[test] +fn new_source_path_without_filename_errors() { + let result = Document::new(valid_metadata(), "body", "url", "out", "", 0, "page", false); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!( + err, + librawssg_error::Error::Validation(ref msg) if msg == "document source_path must have a file name" + )); + } +} + +#[test] +fn new_depth_too_large_errors() { + let result = Document::new( + valid_metadata(), + "body", + "url", + "out", + "src.md", + 1001, + "page", + false, + ); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!( + err, + librawssg_error::Error::Validation(ref msg) if msg == "document depth is unreasonably large" + )); + } +} + +#[test] +fn relative_url_returns_url() { + let doc = valid_document(); + assert_eq!(doc.relative_url(), "blog/my-post.html"); +} + +#[test] +fn add_taxonomy_inserts() { + let mut doc = valid_document(); + doc.add_taxonomy("categories", vec!["rust".to_string(), "ssg".to_string()]); + let tax = doc.taxonomies.get("categories"); + assert!(tax.is_some()); + if let Some(items) = tax { + assert_eq!(items, &vec!["rust".to_string(), "ssg".to_string()]); + } +} + +#[test] +fn depth_returns_depth() { + let doc = valid_document(); + assert_eq!(doc.depth(), 1); +} + +#[test] +fn list_items_default_none() { + let doc = valid_document(); + assert!(doc.list_items.is_none()); +} + +#[test] +fn taxonomies_default_empty() { + let doc = valid_document(); + assert!(doc.taxonomies.is_empty()); +} diff --git a/librawssg_handler/tests/metadata_tests.rs b/librawssg_handler/tests/metadata_tests.rs new file mode 100644 index 0000000..0ff7476 --- /dev/null +++ b/librawssg_handler/tests/metadata_tests.rs @@ -0,0 +1,128 @@ +use librawssg_fs as _; +use librawssg_handler::Metadata; +use serde as _; +use serde_json::json; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +#[test] +fn new_with_valid_title_ok() { + let meta = must!(Metadata::new("My Title", "My Description"), "Metadata::new"); + assert_eq!(meta.title, "My Title"); + assert_eq!(meta.description, "My Description"); + assert!(!meta.draft); + assert!(meta.tags.is_empty()); + assert!(meta.extra.is_empty()); +} + +#[test] +fn new_with_empty_title_errors() { + let result = Metadata::new("", "Desc"); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!( + err, + librawssg_error::Error::Validation(ref msg) if msg == "metadata title cannot be empty" + )); + } +} + +#[test] +fn new_with_whitespace_title_errors() { + let result = Metadata::new(" ", "Desc"); + assert!(result.is_err()); +} + +#[test] +fn is_draft_reflects_field() { + let mut meta = must!(Metadata::new("Title", "Desc"), "Metadata::new"); + assert!(!meta.is_draft()); + meta.draft = true; + assert!(meta.is_draft()); +} + +#[test] +fn insert_extra_and_get() { + let mut meta = must!(Metadata::new("Title", "Desc"), "Metadata::new"); + meta.insert_extra("key", "value"); + let val = meta.get_extra("key"); + assert!(val.is_some()); + if let Some(v) = val { + assert_eq!(v, &json!("value")); + } +} + +#[test] +fn insert_extra_overwrites_existing() { + let mut meta = must!(Metadata::new("Title", "Desc"), "Metadata::new"); + meta.insert_extra("key", "first"); + meta.insert_extra("key", "second"); + let val = meta.get_extra("key"); + assert!(val.is_some()); + if let Some(v) = val { + assert_eq!(v, &json!("second")); + } +} + +#[test] +fn get_extra_missing_returns_none() { + let meta = must!(Metadata::new("Title", "Desc"), "Metadata::new"); + assert!(meta.get_extra("nonexistent").is_none()); +} + +#[test] +fn default_metadata_has_empty_fields() { + let meta = Metadata::default(); + assert_eq!(meta.title, ""); + assert_eq!(meta.description, ""); + assert!(!meta.draft); + assert!(meta.tags.is_empty()); + assert!(meta.extra.is_empty()); + assert!(meta.author.is_none()); + assert!(meta.date.is_none()); + assert!(meta.updated.is_none()); +} + +#[test] +fn serialization_roundtrip() { + let meta = must!(Metadata::new("Title", "Desc"), "Metadata::new"); + let json_str = must!(serde_json::to_string(&meta), "serialize"); + let deserialized: Metadata = must!(serde_json::from_str(&json_str), "deserialize"); + assert_eq!(meta, deserialized); +} + +#[test] +fn deserialize_from_json_with_extra() { + let json_str = r#"{ + "title": "Hello", + "description": "World", + "author": "Alice", + "date": "2026-09-08", + "tags": ["rust", "ssg"], + "draft": false, + "extra": {"foo": "bar"} + }"#; + let meta: Metadata = must!(serde_json::from_str(json_str), "deserialize"); + assert_eq!(meta.title, "Hello"); + assert_eq!(meta.description, "World"); + assert_eq!(meta.author.as_deref(), Some("Alice")); + let expected_date = chrono::NaiveDate::from_ymd_opt(2026, 9, 8); + assert_eq!(meta.date, expected_date); + assert_eq!(meta.tags, vec!["rust", "ssg"]); + assert!(!meta.draft); + let extra_val = meta.get_extra("foo"); + assert!(extra_val.is_some()); + if let Some(v) = extra_val { + assert_eq!(v, &json!("bar")); + } +} diff --git a/librawssg_handler/tests/processor_tests.rs b/librawssg_handler/tests/processor_tests.rs new file mode 100644 index 0000000..3a1145d --- /dev/null +++ b/librawssg_handler/tests/processor_tests.rs @@ -0,0 +1,259 @@ +use chrono as _; +use librawssg_error::Result; +use librawssg_fs::FileSystem; +use librawssg_handler::{Document, Metadata, Processor}; +use serde as _; +use serde_json as _; +use std::io; +use std::path::{Path, PathBuf}; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +struct DummyFs; +impl FileSystem for DummyFs { + fn read_to_string(&self, _path: &Path) -> io::Result { + Err(io::Error::other("not implemented")) + } + fn read_bytes(&self, _path: &Path) -> io::Result> { + Err(io::Error::other("not implemented")) + } + fn write(&self, _path: &Path, _content: &[u8]) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn create_dir_all(&self, _path: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn remove_dir_all(&self, _path: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn remove_file(&self, _path: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn create_dir(&self, _path: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn exists(&self, _path: &Path) -> bool { + false + } + fn is_dir(&self, _path: &Path) -> bool { + false + } + fn is_file(&self, _path: &Path) -> bool { + false + } + fn read_dir(&self, _path: &Path) -> io::Result> { + Err(io::Error::other("not implemented")) + } + fn copy_file(&self, _from: &Path, _to: &Path) -> io::Result { + Err(io::Error::other("not implemented")) + } + fn copy_dir_all(&self, _from: &Path, _to: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn walk_dir(&self, _root: &Path) -> io::Result> { + Err(io::Error::other("not implemented")) + } + fn canonicalize(&self, _path: &Path) -> io::Result { + Err(io::Error::other("not implemented")) + } + fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn atomic_write(&self, _path: &Path, _content: &[u8]) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn touch(&self, _path: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn metadata(&self, _path: &Path) -> io::Result { + Err(io::Error::other("not implemented")) + } + fn symlink_metadata(&self, _path: &Path) -> io::Result { + Err(io::Error::other("not implemented")) + } + fn permissions(&self, _path: &Path) -> io::Result { + Err(io::Error::other("not implemented")) + } + fn set_permissions(&self, _path: &Path, _permissions: std::fs::Permissions) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } + fn read_link(&self, _path: &Path) -> io::Result { + Err(io::Error::other("not implemented")) + } + fn hard_link(&self, _from: &Path, _to: &Path) -> io::Result<()> { + Err(io::Error::other("not implemented")) + } +} + +struct MockProcessor { + name: String, + priority: i32, + can_process_result: bool, + process_output: Option, +} + +impl Processor for MockProcessor { + fn name(&self) -> &str { + &self.name + } + fn priority(&self) -> i32 { + self.priority + } + fn can_process(&self, _relative_path: &Path, _original_path: &Path) -> bool { + self.can_process_result + } + fn process( + &self, + _fs: &dyn FileSystem, + _relative_path: &Path, + _content_dir: &Path, + ) -> Result> { + Ok(self.process_output.clone()) + } +} + +fn sample_document() -> Document { + let metadata = must!(Metadata::new("Sample", "Desc"), "Metadata::new"); + must!( + Document::new( + metadata, + "

Body

", + "sample.html", + "sample/index.html", + "content/sample.md", + 0, + "page", + false, + ), + "Document::new" + ) +} + +#[test] +fn name_returns_set_name() { + let p = MockProcessor { + name: "test".to_string(), + priority: 0, + can_process_result: false, + process_output: None, + }; + assert_eq!(p.name(), "test"); +} + +#[test] +fn priority_default_zero() { + let p = MockProcessor { + name: "default".to_string(), + priority: 0, + can_process_result: false, + process_output: None, + }; + assert_eq!(p.priority(), 0); +} + +#[test] +fn priority_custom() { + let p = MockProcessor { + name: "custom".to_string(), + priority: 5, + can_process_result: false, + process_output: None, + }; + assert_eq!(p.priority(), 5); +} + +#[test] +fn can_process_returns_true() { + let p = MockProcessor { + name: "true".to_string(), + priority: 0, + can_process_result: true, + process_output: None, + }; + assert!(p.can_process(Path::new("a.md"), Path::new("content/a.md"))); +} + +#[test] +fn can_process_returns_false() { + let p = MockProcessor { + name: "false".to_string(), + priority: 0, + can_process_result: false, + process_output: None, + }; + assert!(!p.can_process(Path::new("a.md"), Path::new("content/a.md"))); +} + +#[test] +fn process_returns_document() { + let doc = sample_document(); + let p = MockProcessor { + name: "doc".to_string(), + priority: 0, + can_process_result: true, + process_output: Some(doc.clone()), + }; + let result = must!( + p.process(&DummyFs, Path::new("a.md"), Path::new("content")), + "process" + ); + assert!(result.is_some()); + if let Some(processed) = result { + assert_eq!(processed, doc); + } +} + +#[test] +fn process_returns_none() { + let p = MockProcessor { + name: "none".to_string(), + priority: 0, + can_process_result: true, + process_output: None, + }; + let result = must!( + p.process(&DummyFs, Path::new("a.md"), Path::new("content")), + "process" + ); + assert!(result.is_none()); +} + +#[test] +fn process_error_case() { + struct ErrorProcessor; + impl Processor for ErrorProcessor { + fn name(&self) -> &'static str { + "error" + } + fn can_process(&self, _relative_path: &Path, _original_path: &Path) -> bool { + true + } + fn process( + &self, + _fs: &dyn FileSystem, + _relative_path: &Path, + _content_dir: &Path, + ) -> Result> { + Err(librawssg_error::Error::Processor("test error".into())) + } + } + let p = ErrorProcessor; + let result = p.process(&DummyFs, Path::new("a.md"), Path::new("content")); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!( + err, + librawssg_error::Error::Processor(ref msg) if msg == "test error" + )); + } +} diff --git a/librawssg_handler/tests/unit_tests.rs b/librawssg_handler/tests/unit_tests.rs new file mode 100644 index 0000000..3f6b732 --- /dev/null +++ b/librawssg_handler/tests/unit_tests.rs @@ -0,0 +1,20 @@ +use chrono as _; +use librawssg_error as _; +use librawssg_fs as _; +use serde as _; +use serde_json as _; +#[test] +fn reexports_are_available() { + let _ = librawssg_handler::Metadata::default(); + let _ = librawssg_handler::Document::new( + librawssg_handler::Metadata::default(), + "", + "", + "", + "", + 0, + "", + false, + ); + let _: Option<&dyn librawssg_handler::Processor> = None; +} From acbdf439abb3c6cb2069d904cc7883f59d1ad202 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:09:29 +0700 Subject: [PATCH 08/20] feat(config): implement configuration crate (#31) * chore(lock): update lockfile for config crate deps Add entries for serde_yaml and its dependencies, plus indexmap and unsafe-libyaml. Update existing entries to reflect new dependency graph. * feat(config): add dependencies for config crate Add librawssg_error, serde, serde_json, serde_yaml as dependencies and tempfile as dev-dependency. This enables config parsing, serialization, and testing. * feat(config): expose config modules and types Replace placeholder with module declarations for build, config, content_rule, nav, and site. Re-export public types and add crate-level lint allowance. * chore(handler): specify version for path dependencies Add version = "1.0.0" to librawssg_error and librawssg_fs path dependencies. This clarifies compatibility requirements and aligns with workspace versioning. * feat(config): define BuildConfig struct Add BuildConfig with content_dir, output_dir, templates_dir, static_dir and serde defaults. Implement Default, new, and derive traits for serialization. * feat(config): implement Config with validation and parsing Add Config struct with site, build, content_rules, extra. Provide methods for rule management, validation, YAML/JSON conversion. Validation checks site name, rules, patterns, and base_url. * feat(config): define ContentRule struct Add ContentRule with name, pattern, template, optional list fields, extra. Derive serde traits and provide a constructor. * feat(config): define NavItem struct Add NavItem with label, url, children and serde support. Provide a constructor and Default. * feat(config): define SiteConfig struct Add SiteConfig with nav, sidebar, site_name, description, language, base_url, author, repo_url, license, extra. Implement Default and a constructor. * test(config): add BuildConfig tests Add tests for default values, new equality, YAML roundtrip, and deserialization with defaults. * test(config): add Config tests Add comprehensive tests for Config construction, rule management, validation, YAML/JSON roundtrip, and error cases. * test(config): add ContentRule tests Add tests for constructor, default, and serialization roundtrip. * test(config): add NavItem tests Add tests for constructor, default, child addition, and serialization roundtrip. * test(config): add SiteConfig tests Add tests for constructor, default values, and serialization roundtrip. --- Cargo.lock | 54 ++++ librawssg_config/Cargo.toml | 7 + librawssg_config/src/build.rs | 45 ++++ librawssg_config/src/config.rs | 122 +++++++++ librawssg_config/src/content_rule.rs | 34 +++ librawssg_config/src/lib.rs | 26 +- librawssg_config/src/nav.rs | 20 ++ librawssg_config/src/site.rs | 63 +++++ librawssg_config/tests/build_tests.rs | 50 ++++ librawssg_config/tests/config_tests.rs | 255 +++++++++++++++++++ librawssg_config/tests/content_rule_tests.rs | 51 ++++ librawssg_config/tests/nav_tests.rs | 52 ++++ librawssg_config/tests/site_tests.rs | 51 ++++ librawssg_handler/Cargo.toml | 4 +- 14 files changed, 820 insertions(+), 14 deletions(-) create mode 100644 librawssg_config/src/build.rs create mode 100644 librawssg_config/src/config.rs create mode 100644 librawssg_config/src/content_rule.rs create mode 100644 librawssg_config/src/nav.rs create mode 100644 librawssg_config/src/site.rs create mode 100644 librawssg_config/tests/build_tests.rs create mode 100644 librawssg_config/tests/config_tests.rs create mode 100644 librawssg_config/tests/content_rule_tests.rs create mode 100644 librawssg_config/tests/nav_tests.rs create mode 100644 librawssg_config/tests/site_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 315859d..00f2773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,12 @@ dependencies = [ "serde", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -57,6 +63,22 @@ dependencies = [ "r-efi", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.18" @@ -76,6 +98,13 @@ version = "1.0.0" [[package]] name = "librawssg_config" version = "1.0.0" +dependencies = [ + "librawssg_error", + "serde", + "serde_json", + "serde_yaml", + "tempfile", +] [[package]] name = "librawssg_error" @@ -178,6 +207,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -230,6 +265,19 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "syn" version = "2.0.119" @@ -322,6 +370,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/librawssg_config/Cargo.toml b/librawssg_config/Cargo.toml index 9585972..8e43b21 100644 --- a/librawssg_config/Cargo.toml +++ b/librawssg_config/Cargo.toml @@ -10,6 +10,13 @@ keywords = ["ssg", "static-site-generator", "config", "yaml", "validation"] categories = ["development-tools::build-utils"] [dependencies] +librawssg_error = { version = "1.0.0", path = "../librawssg_error" } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +serde_yaml = "0.9.34" + +[dev-dependencies] +tempfile = "3" [lints] workspace = true diff --git a/librawssg_config/src/build.rs b/librawssg_config/src/build.rs new file mode 100644 index 0000000..aaaabd2 --- /dev/null +++ b/librawssg_config/src/build.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct BuildConfig { + #[serde(default = "default_content_dir")] + pub content_dir: String, + #[serde(default = "default_output_dir")] + pub output_dir: String, + #[serde(default = "default_templates_dir")] + pub templates_dir: String, + #[serde(default = "default_static_dir")] + pub static_dir: String, +} + +fn default_content_dir() -> String { + "content".into() +} +fn default_output_dir() -> String { + "dist".into() +} +fn default_templates_dir() -> String { + "templates".into() +} +fn default_static_dir() -> String { + "static".into() +} + +impl BuildConfig { + #[must_use] + pub fn new() -> Self { + Self::default() + } +} + +impl Default for BuildConfig { + fn default() -> Self { + Self { + content_dir: default_content_dir(), + output_dir: default_output_dir(), + templates_dir: default_templates_dir(), + static_dir: default_static_dir(), + } + } +} diff --git a/librawssg_config/src/config.rs b/librawssg_config/src/config.rs new file mode 100644 index 0000000..874b83d --- /dev/null +++ b/librawssg_config/src/config.rs @@ -0,0 +1,122 @@ +use librawssg_error::{Error, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{BuildConfig, ContentRule, SiteConfig}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[non_exhaustive] +pub struct Config { + pub site: SiteConfig, + pub build: BuildConfig, + pub content_rules: Vec, + #[serde(default)] + pub extra: HashMap, +} + +impl Config { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn with_site_name(mut self, name: impl Into) -> Self { + self.site.site_name = name.into(); + self + } + + pub fn add_content_rule(&mut self, rule: ContentRule) { + self.content_rules.push(rule); + } + + #[must_use] + pub fn find_rule_by_name(&self, name: &str) -> Option<&ContentRule> { + self.content_rules.iter().find(|rule| rule.name == name) + } + + pub fn remove_rule_by_name(&mut self, name: &str) -> Option { + let pos = self.content_rules.iter().position(|rule| rule.name == name); + pos.map(|idx| self.content_rules.remove(idx)) + } + + #[must_use] + pub fn has_duplicate_rule_names(&self) -> bool { + let mut seen = std::collections::HashSet::new(); + self.content_rules + .iter() + .any(|rule| !seen.insert(rule.name.clone())) + } + + pub fn validate(&self) -> Result<()> { + if self.site.site_name.trim().is_empty() { + return Err(Error::Validation("site_name cannot be empty".into())); + } + + if self.content_rules.is_empty() { + return Err(Error::Validation( + "at least one content rule must be defined".into(), + )); + } + + if self.has_duplicate_rule_names() { + return Err(Error::Validation( + "duplicate content rule names are not allowed".into(), + )); + } + + for (idx, rule) in self.content_rules.iter().enumerate() { + if rule.name.trim().is_empty() { + return Err(Error::Validation(format!( + "content rule #{idx}: name cannot be empty" + ))); + } + if rule.pattern.trim().is_empty() { + return Err(Error::Validation(format!( + "content rule '{}': pattern cannot be empty", + rule.name + ))); + } + if rule.pattern.contains("..") { + return Err(Error::Validation(format!( + "content rule '{}': pattern contains invalid '..'", + rule.name + ))); + } + if rule.template.trim().is_empty() { + return Err(Error::Validation(format!( + "content rule '{}': template cannot be empty", + rule.name + ))); + } + } + + if let Some(base_url) = &self.site.base_url + && !(base_url.starts_with("http://") || base_url.starts_with("https://")) + { + return Err(Error::Validation( + "site.base_url must start with http:// or https://".into(), + )); + } + + Ok(()) + } + + pub fn from_yaml_str(yaml: &str) -> Result { + serde_yaml::from_str(yaml).map_err(|e| Error::Config(format!("invalid YAML config: {e}"))) + } + + pub fn to_yaml_string(&self) -> Result { + serde_yaml::to_string(self) + .map_err(|e| Error::Serialization(format!("failed to serialize config: {e}"))) + } + + pub fn from_json_str(json: &str) -> Result { + serde_json::from_str(json).map_err(|e| Error::Config(format!("invalid JSON config: {e}"))) + } + + pub fn to_json_string(&self) -> Result { + serde_json::to_string(self) + .map_err(|e| Error::Serialization(format!("failed to serialize config: {e}"))) + } +} diff --git a/librawssg_config/src/content_rule.rs b/librawssg_config/src/content_rule.rs new file mode 100644 index 0000000..e772eaf --- /dev/null +++ b/librawssg_config/src/content_rule.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[non_exhaustive] +pub struct ContentRule { + pub name: String, + pub pattern: String, + pub template: String, + #[serde(default)] + pub list_template: Option, + #[serde(default)] + pub list_enabled: bool, + #[serde(default)] + pub extra: HashMap, +} + +impl ContentRule { + #[must_use] + pub fn new( + name: impl Into, + pattern: impl Into, + template: impl Into, + ) -> Self { + Self { + name: name.into(), + pattern: pattern.into(), + template: template.into(), + list_template: None, + list_enabled: false, + extra: HashMap::new(), + } + } +} diff --git a/librawssg_config/src/lib.rs b/librawssg_config/src/lib.rs index b93cf3f..6d74594 100644 --- a/librawssg_config/src/lib.rs +++ b/librawssg_config/src/lib.rs @@ -1,14 +1,16 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![allow(clippy::multiple_crate_versions)] -#[cfg(test)] -mod tests { - use super::*; +pub mod build; +pub mod config; +pub mod content_rule; +pub mod nav; +pub mod site; + +pub use build::BuildConfig; +pub use config::Config; +pub use content_rule::ContentRule; +pub use nav::NavItem; +pub use site::SiteConfig; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#[cfg(test)] +use tempfile as _; diff --git a/librawssg_config/src/nav.rs b/librawssg_config/src/nav.rs new file mode 100644 index 0000000..980f701 --- /dev/null +++ b/librawssg_config/src/nav.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[non_exhaustive] +pub struct NavItem { + pub label: String, + pub url: String, + pub children: Vec, +} + +impl NavItem { + #[must_use] + pub fn new(label: impl Into, url: impl Into) -> Self { + Self { + label: label.into(), + url: url.into(), + children: Vec::new(), + } + } +} diff --git a/librawssg_config/src/site.rs b/librawssg_config/src/site.rs new file mode 100644 index 0000000..4682663 --- /dev/null +++ b/librawssg_config/src/site.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct SiteConfig { + #[serde(default)] + pub navbar: Vec, + #[serde(default)] + pub sidebar: Vec, + #[serde(default = "default_site_name")] + pub site_name: String, + #[serde(default)] + pub description: Option, + #[serde(default = "default_language")] + pub language: Option, + #[serde(default)] + pub base_url: Option, + #[serde(default)] + pub author: Option, + #[serde(default)] + pub repo_url: Option, + #[serde(default)] + pub license: Option, + #[serde(default)] + pub extra: HashMap, +} + +fn default_site_name() -> String { + "librawssg".into() +} + +#[allow(clippy::unnecessary_wraps)] +fn default_language() -> Option { + Some("en".into()) +} + +impl SiteConfig { + #[must_use] + pub fn new(site_name: impl Into) -> Self { + Self { + site_name: site_name.into(), + ..Self::default() + } + } +} + +impl Default for SiteConfig { + fn default() -> Self { + Self { + navbar: Vec::new(), + sidebar: Vec::new(), + site_name: default_site_name(), + description: None, + language: default_language(), + base_url: None, + author: None, + repo_url: None, + license: None, + extra: HashMap::new(), + } + } +} diff --git a/librawssg_config/tests/build_tests.rs b/librawssg_config/tests/build_tests.rs new file mode 100644 index 0000000..745b49c --- /dev/null +++ b/librawssg_config/tests/build_tests.rs @@ -0,0 +1,50 @@ +use librawssg_config::BuildConfig; +use librawssg_error as _; +use serde as _; +use serde_json as _; +use tempfile as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +#[test] +fn default_values_are_correct() { + let build = BuildConfig::default(); + assert_eq!(build.content_dir, "content"); + assert_eq!(build.output_dir, "dist"); + assert_eq!(build.templates_dir, "templates"); + assert_eq!(build.static_dir, "static"); +} + +#[test] +fn new_equals_default() { + let build = BuildConfig::new(); + assert_eq!(build, BuildConfig::default()); +} + +#[test] +fn serialize_deserialize_roundtrip_yaml() { + let build = BuildConfig::default(); + let yaml = must!(serde_yaml::to_string(&build), "serialize yaml"); + let parsed: BuildConfig = must!(serde_yaml::from_str(&yaml), "deserialize yaml"); + assert_eq!(build, parsed); +} + +#[test] +fn deserialize_from_yaml_with_defaults() { + let yaml = "content_dir: custom_content\noutput_dir: public\n"; + let build: BuildConfig = must!(serde_yaml::from_str(yaml), "deserialize yaml"); + assert_eq!(build.content_dir, "custom_content"); + assert_eq!(build.output_dir, "public"); + assert_eq!(build.templates_dir, "templates"); + assert_eq!(build.static_dir, "static"); +} diff --git a/librawssg_config/tests/config_tests.rs b/librawssg_config/tests/config_tests.rs new file mode 100644 index 0000000..38ba498 --- /dev/null +++ b/librawssg_config/tests/config_tests.rs @@ -0,0 +1,255 @@ +use librawssg_config::{Config, ContentRule}; +use librawssg_error as _; +use serde as _; +use serde_json::json; +use serde_yaml as _; +use tempfile as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +fn valid_config() -> Config { + let mut config = Config::new().with_site_name("My Site"); + config.add_content_rule(ContentRule::new("page", "**/*.html", "base")); + config +} + +#[test] +fn new_creates_default_with_empty_rules() { + let config = Config::new(); + assert_eq!(config.site.site_name, "librawssg"); + assert!(config.content_rules.is_empty()); + assert!(config.extra.is_empty()); +} + +#[test] +fn with_site_name_updates_name() { + let config = Config::new().with_site_name("Awesome"); + assert_eq!(config.site.site_name, "Awesome"); +} + +#[test] +fn add_and_find_rule() { + let mut config = Config::new(); + config.add_content_rule(ContentRule::new("blog", "**/*.md", "post")); + assert!(config.find_rule_by_name("blog").is_some()); + assert!(config.find_rule_by_name("missing").is_none()); +} + +#[test] +fn remove_rule_by_name_returns_removed() { + let mut config = Config::new(); + config.add_content_rule(ContentRule::new("blog", "**/*.md", "post")); + let removed = config.remove_rule_by_name("blog"); + assert!(removed.is_some()); + assert!(config.find_rule_by_name("blog").is_none()); +} + +#[test] +fn has_duplicate_rule_names_detects_duplicates() { + let mut config = Config::new(); + config.add_content_rule(ContentRule::new("page", "**/*.html", "base")); + config.add_content_rule(ContentRule::new("page", "**/*.raw", "raw")); + assert!(config.has_duplicate_rule_names()); +} + +#[test] +fn valid_config_passes_validation() { + let config = valid_config(); + assert!(config.validate().is_ok()); +} + +#[test] +fn empty_site_name_fails_validation() { + let mut config = valid_config(); + config.site.site_name = " ".to_string(); + let result = config.validate(); + assert!(result.is_err()); + if let Err(err) = result { + assert!( + matches!(err, librawssg_error::Error::Validation(ref msg) if msg == "site_name cannot be empty") + ); + } +} + +#[test] +fn no_content_rules_fails_validation() { + let mut config = valid_config(); + config.content_rules.clear(); + let result = config.validate(); + assert!(result.is_err()); + if let Err(err) = result { + assert!( + matches!(err, librawssg_error::Error::Validation(ref msg) if msg == "at least one content rule must be defined") + ); + } +} + +#[test] +fn duplicate_rule_names_fails_validation() { + let mut config = valid_config(); + config.add_content_rule(ContentRule::new("page", "**/*.raw", "raw")); + let result = config.validate(); + assert!(result.is_err()); + if let Err(err) = result { + assert!( + matches!(err, librawssg_error::Error::Validation(ref msg) if msg == "duplicate content rule names are not allowed") + ); + } +} + +#[test] +fn content_rule_empty_name_fails_validation() { + let mut config = valid_config(); + assert!( + config.content_rules.first_mut().is_some(), + "rule should exist" + ); + if let Some(rule) = config.content_rules.first_mut() { + rule.name = String::new(); + } + let result = config.validate(); + assert!(result.is_err()); +} + +#[test] +fn content_rule_empty_pattern_fails_validation() { + let mut config = valid_config(); + assert!( + config.content_rules.first_mut().is_some(), + "rule should exist" + ); + if let Some(rule) = config.content_rules.first_mut() { + rule.pattern = String::new(); + } + let result = config.validate(); + assert!(result.is_err()); +} + +#[test] +fn content_rule_pattern_with_dotdot_fails_validation() { + let mut config = valid_config(); + assert!( + config.content_rules.first_mut().is_some(), + "rule should exist" + ); + if let Some(rule) = config.content_rules.first_mut() { + rule.pattern = "**/../*.html".to_string(); + } + let result = config.validate(); + assert!(result.is_err()); + if let Err(err) = result { + assert!( + matches!(err, librawssg_error::Error::Validation(ref msg) if msg.contains("contains invalid '..'")) + ); + } +} + +#[test] +fn content_rule_empty_template_fails_validation() { + let mut config = valid_config(); + assert!( + config.content_rules.first_mut().is_some(), + "rule should exist" + ); + if let Some(rule) = config.content_rules.first_mut() { + rule.template = String::new(); + } + let result = config.validate(); + assert!(result.is_err()); +} + +#[test] +fn invalid_base_url_fails_validation() { + let mut config = valid_config(); + config.site.base_url = Some("ftp://example.com".to_string()); + let result = config.validate(); + assert!(result.is_err()); + if let Err(err) = result { + assert!( + matches!(err, librawssg_error::Error::Validation(ref msg) if msg == "site.base_url must start with http:// or https://") + ); + } +} + +#[test] +fn valid_base_url_passes_validation() { + let mut config = valid_config(); + config.site.base_url = Some("https://example.com".to_string()); + assert!(config.validate().is_ok()); +} + +#[test] +fn yaml_roundtrip() { + let config = valid_config(); + let yaml = must!(config.to_yaml_string(), "to_yaml_string"); + let parsed = must!(Config::from_yaml_str(&yaml), "from_yaml_str"); + assert_eq!(config, parsed); +} + +#[test] +fn json_roundtrip() { + let config = valid_config(); + let json = must!(config.to_json_string(), "to_json_string"); + let parsed = must!(Config::from_json_str(&json), "from_json_str"); + assert_eq!(config, parsed); +} + +#[test] +fn deserialize_from_yaml_with_extra_fields() { + let yaml = r#" +site: + site_name: Test Site + description: A test + extra: + foo: bar +build: + content_dir: src +content_rules: + - name: page + pattern: "**/*.raw" + template: "main" + list_enabled: true +"#; + let config = must!(Config::from_yaml_str(yaml), "from_yaml_str"); + assert_eq!(config.site.site_name, "Test Site"); + assert_eq!(config.build.content_dir, "src"); + assert_eq!(config.content_rules.len(), 1); + assert!(!config.content_rules.is_empty(), "rule should exist"); + if let Some(rule) = config.content_rules.first() { + assert_eq!(rule.name, "page"); + assert!(rule.list_enabled); + } + let extra = config.site.extra.get("foo"); + assert!(extra.is_some()); + if let Some(value) = extra { + assert_eq!(value, &json!("bar")); + } +} + +#[test] +fn deserialize_invalid_yaml_returns_config_error() { + let result = Config::from_yaml_str("invalid_yaml: ["); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!(err, librawssg_error::Error::Config(_))); + } +} + +#[test] +fn deserialize_invalid_json_returns_config_error() { + let result = Config::from_json_str("{invalid json"); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!(err, librawssg_error::Error::Config(_))); + } +} diff --git a/librawssg_config/tests/content_rule_tests.rs b/librawssg_config/tests/content_rule_tests.rs new file mode 100644 index 0000000..527105e --- /dev/null +++ b/librawssg_config/tests/content_rule_tests.rs @@ -0,0 +1,51 @@ +use librawssg_config::ContentRule; +use librawssg_error as _; +use serde as _; +use serde_json::json; +use serde_yaml as _; +use tempfile as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +#[test] +fn new_sets_required_fields() { + let rule = ContentRule::new("blog", "**/*.md", "post"); + assert_eq!(rule.name, "blog"); + assert_eq!(rule.pattern, "**/*.md"); + assert_eq!(rule.template, "post"); + assert!(!rule.list_enabled); + assert!(rule.list_template.is_none()); + assert!(rule.extra.is_empty()); +} + +#[test] +fn default_is_empty() { + let rule = ContentRule::default(); + assert_eq!(rule.name, ""); + assert_eq!(rule.pattern, ""); + assert_eq!(rule.template, ""); + assert!(!rule.list_enabled); + assert!(rule.list_template.is_none()); + assert!(rule.extra.is_empty()); +} + +#[test] +fn serialize_deserialize_roundtrip() { + let mut rule = ContentRule::new("page", "**/*.html", "base"); + rule.list_enabled = true; + rule.list_template = Some("list".into()); + let _ = rule.extra.insert("key".into(), json!("value")); + let yaml = must!(serde_yaml::to_string(&rule), "serialize"); + let parsed: ContentRule = must!(serde_yaml::from_str(&yaml), "deserialize"); + assert_eq!(rule, parsed); +} diff --git a/librawssg_config/tests/nav_tests.rs b/librawssg_config/tests/nav_tests.rs new file mode 100644 index 0000000..2b2e67e --- /dev/null +++ b/librawssg_config/tests/nav_tests.rs @@ -0,0 +1,52 @@ +use librawssg_config::NavItem; +use librawssg_error as _; +use serde as _; +use serde_yaml as _; +use tempfile as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +#[test] +fn new_sets_label_and_url() { + let item = NavItem::new("Home", "/"); + assert_eq!(item.label, "Home"); + assert_eq!(item.url, "/"); + assert!(item.children.is_empty()); +} + +#[test] +fn default_is_empty() { + let item = NavItem::default(); + assert_eq!(item.label, ""); + assert_eq!(item.url, ""); + assert!(item.children.is_empty()); +} + +#[test] +fn children_can_be_added() { + let mut parent = NavItem::new("Docs", "/docs"); + parent.children.push(NavItem::new("API", "/docs/api")); + assert_eq!(parent.children.len(), 1); + assert!(!parent.children.is_empty(), "child should exist"); + if let Some(child) = parent.children.first() { + assert_eq!(child.label, "API"); + } +} + +#[test] +fn serialize_deserialize_roundtrip() { + let item = NavItem::new("Home", "/"); + let json = must!(serde_json::to_string(&item), "serialize"); + let parsed: NavItem = must!(serde_json::from_str(&json), "deserialize"); + assert_eq!(item, parsed); +} diff --git a/librawssg_config/tests/site_tests.rs b/librawssg_config/tests/site_tests.rs new file mode 100644 index 0000000..b910db3 --- /dev/null +++ b/librawssg_config/tests/site_tests.rs @@ -0,0 +1,51 @@ +use librawssg_config::SiteConfig; +use librawssg_error as _; +use serde as _; +use serde_json::json; +use serde_yaml as _; +use tempfile as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +#[test] +fn new_sets_site_name_only() { + let site = SiteConfig::new("My Site"); + assert_eq!(site.site_name, "My Site"); + assert_eq!(site.language.as_deref(), Some("en")); + assert!(site.navbar.is_empty()); + assert!(site.sidebar.is_empty()); +} + +#[test] +fn default_has_expected_values() { + let site = SiteConfig::default(); + assert_eq!(site.site_name, "librawssg"); + assert_eq!(site.language.as_deref(), Some("en")); + assert!(site.navbar.is_empty()); + assert!(site.sidebar.is_empty()); + assert!(site.description.is_none()); + assert!(site.base_url.is_none()); + assert!(site.author.is_none()); + assert!(site.repo_url.is_none()); + assert!(site.license.is_none()); + assert!(site.extra.is_empty()); +} + +#[test] +fn serialize_deserialize_roundtrip() { + let mut site = SiteConfig::new("Test"); + let _ = site.extra.insert("foo".into(), json!("bar")); + let yaml = must!(serde_yaml::to_string(&site), "serialize"); + let parsed: SiteConfig = must!(serde_yaml::from_str(&yaml), "deserialize"); + assert_eq!(site, parsed); +} diff --git a/librawssg_handler/Cargo.toml b/librawssg_handler/Cargo.toml index 829e237..c77e3ad 100644 --- a/librawssg_handler/Cargo.toml +++ b/librawssg_handler/Cargo.toml @@ -10,8 +10,8 @@ keywords = ["ssg", "static-site-generator", "content", "processor", "traits"] categories = ["development-tools::build-utils"] [dependencies] -librawssg_error = { path = "../librawssg_error" } -librawssg_fs = { path = "../librawssg_fs" } +librawssg_error = { version = "1.0.0", path = "../librawssg_error" } +librawssg_fs = { version = "1.0.0", path = "../librawssg_fs" } chrono = { version = "0.4", default-features = false, features = ["serde"] } serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" From 2707a185ba2941008bdc22b586c9998161b3f968 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:24:09 +0700 Subject: [PATCH 09/20] feat(templates): add renderer traits and Tera implementation (#32) * chore(lock): update lockfile for templates crate dependencies Add dependencies for tera and its transitive crates (chrono-tz, globwalk, pest, slug, etc.) to Cargo.lock. This reflects the new optional tera feature in librawssg_templates and keeps the lockfile in sync with Cargo.toml changes. * feat(templates): add tera feature and dependencies Add optional tera feature with default enabled. Add librawssg_error, walkdir, and tera dependencies. Add tempfile as dev-dependency. Update keywords to include tera. * feat(templates): expose renderer and tera modules Replace placeholder with module declarations for renderer and optional tera_renderer. Re-export public types and add crate-level lint allowance for multiple crate versions. * feat(templates): define RenderContext and Renderer traits Add RenderContext trait with as_any and as_mut_any methods. Add Renderer trait with render method returning Result. * feat(templates): implement TeraRenderer Add TeraRenderer struct with methods for adding raw templates, loading from files/directories, enabling autoescape, and rendering. Implement Renderer and RenderContext for tera::Context. * test(templates): add TeraRenderer integration tests Add comprehensive tests for TeraRenderer including raw templates, loops, filters, conditions, file loading, recursion, autoescape, inheritance, macros, includes, and mutable access. * test(templates): add basic trait and reexport tests Add unit tests verifying Renderer and RenderContext traits can be mocked and reexports are available. --- Cargo.lock | 611 ++++++++++++++++++++++- librawssg_templates/Cargo.toml | 15 +- librawssg_templates/src/lib.rs | 22 +- librawssg_templates/src/renderer.rs | 12 + librawssg_templates/src/tera_renderer.rs | 140 ++++++ librawssg_templates/tests/tera_tests.rs | 282 +++++++++++ librawssg_templates/tests/unit_tests.rs | 55 ++ 7 files changed, 1123 insertions(+), 14 deletions(-) create mode 100644 librawssg_templates/src/renderer.rs create mode 100644 librawssg_templates/src/tera_renderer.rs create mode 100644 librawssg_templates/tests/tera_tests.rs create mode 100644 librawssg_templates/tests/unit_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 00f2773..71a5a0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,24 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -14,6 +32,32 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -26,10 +70,71 @@ version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ + "iana-time-zone", "num-traits", "serde", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "equivalent" version = "1.0.2" @@ -52,6 +157,47 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -63,12 +209,85 @@ dependencies = [ "r-efi", ] +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -85,12 +304,35 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "librawssg_compiler" version = "1.0.0" @@ -136,6 +378,12 @@ dependencies = [ [[package]] name = "librawssg_templates" version = "1.0.0" +dependencies = [ + "librawssg_error", + "tempfile", + "tera", + "walkdir", +] [[package]] name = "linux-raw-sys" @@ -143,6 +391,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + [[package]] name = "memchr" version = "2.8.3" @@ -164,12 +418,116 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e" +dependencies = [ + "pest", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -194,6 +552,65 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "rustix" version = "1.1.4" @@ -207,6 +624,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "ryu" version = "1.0.23" @@ -278,6 +701,34 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "syn" version = "2.0.119" @@ -307,12 +758,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", ] +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + [[package]] name = "thiserror" version = "2.0.20" @@ -364,12 +837,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -386,6 +871,57 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -395,12 +931,65 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -410,6 +999,26 @@ dependencies = [ "windows-link", ] +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/librawssg_templates/Cargo.toml b/librawssg_templates/Cargo.toml index 6fecaf7..28629ed 100644 --- a/librawssg_templates/Cargo.toml +++ b/librawssg_templates/Cargo.toml @@ -6,10 +6,23 @@ description = "Template rendering contracts for the librawssg static site genera license = "MIT" repository = "https://github.com/mroczect/librawssg" readme = "README.md" -keywords = ["ssg", "static-site-generator", "template", "renderer", "traits"] +keywords = ["ssg", "static-site-generator", "template", "renderer", "tera"] categories = ["development-tools::build-utils"] +[features] +default = ["tera"] +tera = ["dep:tera"] + [dependencies] +librawssg_error = { version = "1.0.0", path = "../librawssg_error" } +walkdir = "2.5.0" + +[dependencies.tera] +version = "1.20.0" +optional = true + +[dev-dependencies] +tempfile = "3" [lints] workspace = true diff --git a/librawssg_templates/src/lib.rs b/librawssg_templates/src/lib.rs index b93cf3f..7c32251 100644 --- a/librawssg_templates/src/lib.rs +++ b/librawssg_templates/src/lib.rs @@ -1,14 +1,12 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![allow(clippy::multiple_crate_versions)] -#[cfg(test)] -mod tests { - use super::*; +pub mod renderer; +#[cfg(feature = "tera")] +pub mod tera_renderer; + +pub use renderer::{RenderContext, Renderer}; +#[cfg(feature = "tera")] +pub use tera_renderer::TeraRenderer; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#[cfg(test)] +use tempfile as _; diff --git a/librawssg_templates/src/renderer.rs b/librawssg_templates/src/renderer.rs new file mode 100644 index 0000000..661367c --- /dev/null +++ b/librawssg_templates/src/renderer.rs @@ -0,0 +1,12 @@ +use core::any::Any; +use librawssg_error::Result; + +pub trait RenderContext: Send + Sync { + fn as_any(&self) -> &dyn Any; + + fn as_mut_any(&mut self) -> &mut dyn Any; +} + +pub trait Renderer: Send + Sync { + fn render(&self, template_name: &str, context: &dyn RenderContext) -> Result; +} diff --git a/librawssg_templates/src/tera_renderer.rs b/librawssg_templates/src/tera_renderer.rs new file mode 100644 index 0000000..b90d8c9 --- /dev/null +++ b/librawssg_templates/src/tera_renderer.rs @@ -0,0 +1,140 @@ +use crate::renderer::{RenderContext, Renderer}; +use librawssg_error::{Error, Result}; +use std::path::Path; + +#[derive(Debug)] +pub struct TeraRenderer { + tera: tera::Tera, +} + +impl TeraRenderer { + #[must_use] + pub fn new() -> Self { + Self { + tera: tera::Tera::default(), + } + } + + pub fn add_raw_template(&mut self, name: &str, content: &str) -> Result<()> { + self.tera + .add_raw_template(name, content) + .map_err(|e| Error::Render(e.to_string())) + } + + pub fn add_template_file(&mut self, path: &Path) -> Result<()> { + let content = std::fs::read_to_string(path) + .map_err(|e| Error::Io(std::io::Error::other(format!("{e}"))))?; + let name = path + .file_name() + .and_then(|s| s.to_str()) + .ok_or_else(|| Error::Render("template file has no valid file name".into()))?; + self.add_raw_template(name, &content) + } + + pub fn add_template_files_from_dir(&mut self, dir: &Path) -> Result<()> { + let entries = + std::fs::read_dir(dir).map_err(|e| Error::Io(std::io::Error::other(format!("{e}"))))?; + for entry in entries { + let entry = entry.map_err(|e| Error::Io(std::io::Error::other(format!("{e}"))))?; + let path = entry.path(); + if path.is_file() { + self.add_template_file(&path)?; + } + } + Ok(()) + } + + pub fn load_templates_dir(&mut self, dir: &Path) -> Result<()> { + let dir_canon = dir + .canonicalize() + .map_err(|e| Error::Io(std::io::Error::other(format!("{e}"))))?; + for entry in walkdir::WalkDir::new(&dir_canon) { + let entry = entry.map_err(|e| Error::Io(std::io::Error::other(format!("{e}"))))?; + if entry.file_type().is_file() { + let abs_path = entry.path(); + let rel_path = abs_path + .strip_prefix(&dir_canon) + .map_err(|e| Error::Io(std::io::Error::other(format!("{e}"))))?; + let template_name = rel_path_to_template_name(rel_path)?; + let content = std::fs::read_to_string(abs_path) + .map_err(|e| Error::Io(std::io::Error::other(format!("{e}"))))?; + self.add_raw_template(&template_name, &content)?; + } + } + Ok(()) + } + + pub fn enable_autoescape(&mut self) { + self.tera.autoescape_on(vec!["html", "htm", "xml"]); + } + + pub fn render_str(&self, template_str: &str, context: &dyn RenderContext) -> Result { + let tera_ctx = context + .as_any() + .downcast_ref::() + .ok_or_else(|| Error::Render("invalid context type for Tera".into()))?; + tera::Tera::one_off(template_str, tera_ctx, true).map_err(|e| Error::Render(e.to_string())) + } + + #[must_use] + pub const fn as_tera(&self) -> &tera::Tera { + &self.tera + } + + #[must_use] + pub const fn as_tera_mut(&mut self) -> &mut tera::Tera { + &mut self.tera + } +} + +fn rel_path_to_template_name(rel_path: &Path) -> Result { + let mut parts: Vec<&str> = Vec::new(); + for component in rel_path.components() { + match component { + std::path::Component::Normal(os_str) => parts.push( + os_str + .to_str() + .ok_or_else(|| Error::Render("non-UTF-8 path".into()))?, + ), + std::path::Component::Prefix(_) + | std::path::Component::RootDir + | std::path::Component::CurDir + | std::path::Component::ParentDir => { + return Err(Error::Render( + "unexpected path component in template path".into(), + )); + } + } + } + if parts.is_empty() { + return Err(Error::Render("empty template name".into())); + } + Ok(parts.join("/")) +} + +impl Default for TeraRenderer { + fn default() -> Self { + Self::new() + } +} + +impl Renderer for TeraRenderer { + fn render(&self, template_name: &str, context: &dyn RenderContext) -> Result { + let tera_ctx = context + .as_any() + .downcast_ref::() + .ok_or_else(|| Error::Render("invalid context type for Tera".into()))?; + self.tera + .render(template_name, tera_ctx) + .map_err(|e| Error::Render(e.to_string())) + } +} + +impl RenderContext for tera::Context { + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_mut_any(&mut self) -> &mut dyn core::any::Any { + self + } +} diff --git a/librawssg_templates/tests/tera_tests.rs b/librawssg_templates/tests/tera_tests.rs new file mode 100644 index 0000000..c6c78a5 --- /dev/null +++ b/librawssg_templates/tests/tera_tests.rs @@ -0,0 +1,282 @@ +#![cfg(feature = "tera")] + +use librawssg_templates::{RenderContext, Renderer, TeraRenderer}; +use tempfile as _; +use walkdir as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +fn sample_context() -> tera::Context { + let mut ctx = tera::Context::new(); + ctx.insert("title", "Hello"); + ctx.insert("items", &vec!["a", "b", "c"]); + ctx.insert("number", &42); + ctx +} + +#[test] +fn render_raw_template_with_simple_variable() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template("simple", "{{ title }}"), + "add_raw_template" + ); + + let mut ctx = tera::Context::new(); + ctx.insert("title", "Hello World"); + let output = must!(renderer.render("simple", &ctx), "render"); + assert_eq!(output, "Hello World"); +} + +#[test] +fn render_raw_template_with_loop() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template( + "loop", + "{% for item in items %}{{ item }}{% if not loop.last %},{% endif %}{% endfor %}" + ), + "add_raw_template" + ); + + let ctx = sample_context(); + let output = must!(renderer.render("loop", &ctx), "render"); + assert_eq!(output, "a,b,c"); +} + +#[test] +fn render_raw_template_with_filter() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template("filter", "{{ title | upper }}"), + "add_raw_template" + ); + + let ctx = sample_context(); + let output = must!(renderer.render("filter", &ctx), "render"); + assert_eq!(output, "HELLO"); +} + +#[test] +fn render_raw_template_with_condition() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template( + "condition", + "{% if number > 40 %}high{% else %}low{% endif %}" + ), + "add_raw_template" + ); + + let ctx = sample_context(); + let output = must!(renderer.render("condition", &ctx), "render"); + assert_eq!(output, "high"); +} + +#[test] +fn render_missing_template_errors() { + let renderer = TeraRenderer::new(); + let ctx = tera::Context::new(); + let result = renderer.render("missing", &ctx); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!(err, librawssg_error::Error::Render(_))); + } +} + +#[test] +fn render_with_missing_variable_errors() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template("missing_var", "{{ does_not_exist }}"), + "add_raw_template" + ); + let ctx = tera::Context::new(); + let result = renderer.render("missing_var", &ctx); + assert!(result.is_err()); +} + +#[test] +fn add_invalid_template_errors() { + let mut renderer = TeraRenderer::new(); + let result = renderer.add_raw_template("invalid", "{% if %}"); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!(err, librawssg_error::Error::Render(_))); + } +} + +#[test] +fn add_template_file_works() { + let dir = must!(tempfile::TempDir::new(), "TempDir::new"); + let file_path = dir.path().join("hello.tera"); + must!(std::fs::write(&file_path, "{{ name }}"), "write template"); + + let mut renderer = TeraRenderer::new(); + must!(renderer.add_template_file(&file_path), "add_template_file"); + + let mut ctx = tera::Context::new(); + ctx.insert("name", "World"); + let output = must!(renderer.render("hello.tera", &ctx), "render"); + assert_eq!(output, "World"); +} + +#[test] +fn add_template_files_from_dir_works() { + let dir = must!(tempfile::TempDir::new(), "TempDir::new"); + must!(std::fs::write(dir.path().join("a.tera"), "A"), "write a"); + must!(std::fs::write(dir.path().join("b.tera"), "B"), "write b"); + + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_template_files_from_dir(dir.path()), + "add from dir" + ); + + let ctx = tera::Context::new(); + let out_a = must!(renderer.render("a.tera", &ctx), "render a"); + let out_b = must!(renderer.render("b.tera", &ctx), "render b"); + assert_eq!(out_a, "A"); + assert_eq!(out_b, "B"); +} + +#[test] +fn load_templates_dir_recursive() { + let dir = must!(tempfile::TempDir::new(), "TempDir::new"); + let sub_dir = dir.path().join("sub"); + must!(std::fs::create_dir(&sub_dir), "create subdir"); + must!( + std::fs::write(sub_dir.join("nested.tera"), "Nested"), + "write nested" + ); + + let mut renderer = TeraRenderer::new(); + must!( + renderer.load_templates_dir(dir.path()), + "load templates dir" + ); + + let ctx = tera::Context::new(); + let output = must!(renderer.render("sub/nested.tera", &ctx), "render nested"); + assert_eq!(output, "Nested"); +} + +#[test] +fn autoescape_escapes_html() { + let mut renderer = TeraRenderer::new(); + renderer.enable_autoescape(); + must!( + renderer.add_raw_template("esc.html", "{{ content }}"), + "add_raw_template" + ); + let mut ctx = tera::Context::new(); + ctx.insert("content", ""); + let output = must!(renderer.render("esc.html", &ctx), "render"); + assert_eq!(output, "<script>alert(1)</script>"); +} + +#[test] +fn render_str_autoescapes_by_default() { + let renderer = TeraRenderer::new(); + let mut ctx = tera::Context::new(); + ctx.insert("content", "bold"); + let output = must!(renderer.render_str("{{ content }}", &ctx), "render_str"); + assert_eq!(output, "<b>bold</b>"); +} + +#[test] +fn inheritance_and_blocks() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template( + "base", + "{% block content %}Default{% endblock %}" + ), + "add base" + ); + must!( + renderer.add_raw_template( + "child", + "{% extends \"base\" %}{% block content %}Child content{% endblock %}" + ), + "add child" + ); + + let ctx = tera::Context::new(); + let output = must!(renderer.render("child", &ctx), "render child"); + assert_eq!(output, "Child content"); +} + +#[test] +fn macros_work() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template( + "macro", + "{% macro hello(name) %}Hello, {{ name }}{% endmacro hello %}{{ self::hello(name=\"World\") }}" + ), + "add macro template" + ); + let ctx = tera::Context::new(); + let output = must!(renderer.render("macro", &ctx), "render macro"); + assert_eq!(output, "Hello, World"); +} + +#[test] +fn include_partial_works() { + let mut renderer = TeraRenderer::new(); + must!( + renderer.add_raw_template("partial", "Partial content"), + "add partial" + ); + must!( + renderer.add_raw_template("main", "{% include \"partial\" %}"), + "add main" + ); + let ctx = tera::Context::new(); + let output = must!(renderer.render("main", &ctx), "render main"); + assert_eq!(output, "Partial content"); +} + +#[test] +fn context_downcast_works() { + let ctx = sample_context(); + let dyn_ctx: &dyn RenderContext = &ctx; + assert!(dyn_ctx.as_any().is::()); +} + +#[test] +fn context_mutable_downcast_works() { + let mut ctx = sample_context(); + let dyn_ctx: &mut dyn RenderContext = &mut ctx; + assert!(dyn_ctx.as_mut_any().is::()); +} + +#[test] +fn as_tera_returns_underlying_engine() { + let renderer = TeraRenderer::new(); + let tera = renderer.as_tera(); + let _ = tera; +} +#[test] +fn as_tera_mut_allows_mutation() { + let mut renderer = TeraRenderer::new(); + let tera_mut = renderer.as_tera_mut(); + let add_result = tera_mut + .add_raw_template("via_mut", "ok") + .map_err(|e| librawssg_error::Error::Render(e.to_string())); + must!(add_result, "add raw template via mutable access"); + let ctx = tera::Context::new(); + let output = must!(renderer.render("via_mut", &ctx), "render via mut"); + assert_eq!(output, "ok"); +} diff --git a/librawssg_templates/tests/unit_tests.rs b/librawssg_templates/tests/unit_tests.rs new file mode 100644 index 0000000..5984a33 --- /dev/null +++ b/librawssg_templates/tests/unit_tests.rs @@ -0,0 +1,55 @@ +use core::any::Any; +use librawssg_error::Result; +use librawssg_templates::{RenderContext, Renderer}; +use tempfile as _; +use tera as _; +use walkdir as _; +struct MockRenderer { + output: String, +} + +impl Renderer for MockRenderer { + fn render(&self, _template_name: &str, _context: &dyn RenderContext) -> Result { + Ok(self.output.clone()) + } +} + +struct MockContext; + +impl RenderContext for MockContext { + fn as_any(&self) -> &dyn Any { + self + } + fn as_mut_any(&mut self) -> &mut dyn Any { + self + } +} + +#[test] +fn renderer_trait_can_be_mocked() { + let renderer = MockRenderer { + output: "mock".to_string(), + }; + let ctx = MockContext; + let output = match renderer.render("whatever", &ctx) { + Ok(value) => value, + Err(e) => { + eprintln!("render failed: {}", e); + std::process::exit(1); + } + }; + assert_eq!(output, "mock"); +} + +#[test] +fn render_context_trait_can_be_mocked() { + let ctx = MockContext; + let dyn_ctx: &dyn RenderContext = &ctx; + assert!(dyn_ctx.as_any().is::()); +} + +#[test] +fn reexports_available() { + let _: Option<&dyn Renderer> = None; + let _: Option<&dyn RenderContext> = None; +} From 20e5d354526b832ef9caeacf014c57d3f12a9286 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:05 +0700 Subject: [PATCH 10/20] chore(lock): update lockfile for compiler dependencies Add librawssg_config, librawssg_handler, librawssg_templates, and tempfile dependencies to compiler crate entry. This reflects new compiler Cargo.toml dependencies and keeps the lockfile synchronized. --- Cargo.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 71a5a0d..47f876e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "librawssg_compiler" version = "1.0.0" +dependencies = [ + "librawssg_config", + "librawssg_error", + "librawssg_fs", + "librawssg_handler", + "librawssg_templates", + "tempfile", +] [[package]] name = "librawssg_config" From 9effd0b6e1e4352b7580328423f124ce996f08e6 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:06 +0700 Subject: [PATCH 11/20] feat(compiler): add dependencies for compiler crate Add librawssg_error, librawssg_fs, librawssg_config, librawssg_handler, and librawssg_templates as runtime dependencies. Add tempfile as dev-dependency. This prepares the compiler crate for building the pipeline. --- librawssg_compiler/Cargo.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/librawssg_compiler/Cargo.toml b/librawssg_compiler/Cargo.toml index 8590eaf..8bd2882 100644 --- a/librawssg_compiler/Cargo.toml +++ b/librawssg_compiler/Cargo.toml @@ -10,6 +10,14 @@ keywords = ["ssg", "static-site-generator", "pipeline", "builder", "compiler"] categories = ["development-tools::build-utils"] [dependencies] +librawssg_error = { version = "1.0.0", path = "../librawssg_error" } +librawssg_fs = { version = "1.0.0", path = "../librawssg_fs" } +librawssg_config = { version = "1.0.0", path = "../librawssg_config" } +librawssg_handler = { version = "1.0.0", path = "../librawssg_handler" } +librawssg_templates = { version = "1.0.0", path = "../librawssg_templates" } + +[dev-dependencies] +tempfile = "3" [lints] workspace = true From 1c52455c5cfc089cdc0d6dd170935fbb0628a089 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:06 +0700 Subject: [PATCH 12/20] feat(compiler): expose public modules and types Replace placeholder with module declarations for builder, context, generator, pattern, and pipeline. Re-export PipelineBuilder, ContextBuilder, Generator, Pipeline, and TeraContextBuilder under tera feature. --- librawssg_compiler/src/lib.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/librawssg_compiler/src/lib.rs b/librawssg_compiler/src/lib.rs index b93cf3f..9f8007d 100644 --- a/librawssg_compiler/src/lib.rs +++ b/librawssg_compiler/src/lib.rs @@ -1,14 +1,15 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![allow(clippy::multiple_crate_versions)] -#[cfg(test)] -mod tests { - use super::*; +pub mod builder; +pub mod context; +pub mod generator; +pub mod pattern; +pub mod pipeline; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use builder::PipelineBuilder; +pub use context::ContextBuilder; +pub use generator::Generator; +pub use pipeline::Pipeline; + +#[cfg(feature = "tera")] +pub use context::TeraContextBuilder; From 42f57e7a0afc8646495673291c30cfc26d353402 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:06 +0700 Subject: [PATCH 13/20] feat(compiler): implement PipelineBuilder Add PipelineBuilder struct with config, content/output directories, filesystem, renderer, processors, context builder, and generators. Implement builder methods and `build` method that validates config and constructs a Pipeline. --- librawssg_compiler/src/builder.rs | 129 ++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 librawssg_compiler/src/builder.rs diff --git a/librawssg_compiler/src/builder.rs b/librawssg_compiler/src/builder.rs new file mode 100644 index 0000000..bdb7853 --- /dev/null +++ b/librawssg_compiler/src/builder.rs @@ -0,0 +1,129 @@ +use super::ContextBuilder; +use super::processor::Processor; +use crate::generator::Generator; +use crate::pipeline::Pipeline; +use librawssg_config::Config; +use librawssg_error::Result; +use librawssg_fs::FileSystem; +use librawssg_fs::RealFs; +use librawssg_templates::Renderer; +use std::path::PathBuf; + +pub struct PipelineBuilder { + config: Config, + content_dir: PathBuf, + output_dir: PathBuf, + fs: Box, + renderer: Option>, + processors: Vec>, + context_builder: Option>, + generators: Vec>, +} + +impl PipelineBuilder { + #[must_use] + pub fn new() -> Self { + Self { + config: Config::default(), + content_dir: PathBuf::from("content"), + output_dir: PathBuf::from("dist"), + fs: Box::new(RealFs), + renderer: None, + processors: Vec::new(), + context_builder: None, + generators: Vec::new(), + } + } + + #[must_use] + pub fn config(mut self, config: Config) -> Self { + self.config = config; + self + } + + pub fn load_config + Send + Sync>(mut self, path: P) -> Result { + let content = std::fs::read_to_string(path.as_ref()) + .map_err(|e| librawssg_error::Error::Config(e.to_string()))?; + self.config = Config::from_yaml_str(&content)?; + Ok(self) + } + + #[must_use] + pub fn content_dir(mut self, dir: impl Into) -> Self { + self.content_dir = dir.into(); + self + } + + #[must_use] + pub fn output_dir(mut self, dir: impl Into) -> Self { + self.output_dir = dir.into(); + self + } + + #[must_use] + pub fn with_fs(mut self, fs: Box) -> Self { + self.fs = fs; + self + } + + #[must_use] + pub fn with_renderer(mut self, renderer: Box) -> Self { + self.renderer = Some(renderer); + self + } + + #[must_use] + pub fn add_processor(mut self, processor: Box) -> Self { + self.processors.push(processor); + self + } + + #[must_use] + pub fn with_context_builder(mut self, builder: Box) -> Self { + self.context_builder = Some(builder); + self + } + + #[must_use] + pub fn add_generator(mut self, generator: Box) -> Self { + self.generators.push(generator); + self + } + + pub fn build(mut self) -> Result { + self.config.validate()?; + + if self.content_dir == PathBuf::from("content") { + self.content_dir = PathBuf::from(&self.config.build.content_dir); + } + if self.output_dir == PathBuf::from("dist") { + self.output_dir = PathBuf::from(&self.config.build.output_dir); + } + + let renderer = self + .renderer + .take() + .ok_or_else(|| librawssg_error::Error::Config("template renderer not set".into()))?; + let context_builder = self + .context_builder + .take() + .ok_or_else(|| librawssg_error::Error::Config("context builder not set".into()))?; + + Ok(Pipeline { + config: self.config, + fs: self.fs, + renderer, + processors: self.processors, + context_builder, + generators: self.generators, + content_dir: self.content_dir, + output_dir: self.output_dir, + }) + } +} + +impl Default for PipelineBuilder { + fn default() -> Self { + Self::new() + } +} From 5713c747755f034ad2ee39568d9513f9e79394ee Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:06 +0700 Subject: [PATCH 14/20] feat(compiler): define ContextBuilder trait and Tera implementation Add ContextBuilder trait that builds a RenderContext from Config and Document. Implement TeraContextBuilder under the `tera` feature, populating a tera::Context with site and page fields. --- librawssg_compiler/src/context.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 librawssg_compiler/src/context.rs diff --git a/librawssg_compiler/src/context.rs b/librawssg_compiler/src/context.rs new file mode 100644 index 0000000..c63174b --- /dev/null +++ b/librawssg_compiler/src/context.rs @@ -0,0 +1,31 @@ +use librawssg_config::Config; +use librawssg_error::Result; +use librawssg_handler::Document; +use librawssg_templates::RenderContext; + +pub trait ContextBuilder: Send + Sync { + fn build_context(&self, config: &Config, doc: &Document) -> Result>; +} + +#[cfg(feature = "tera")] +pub struct TeraContextBuilder; + +#[cfg(feature = "tera")] +impl ContextBuilder for TeraContextBuilder { + fn build_context(&self, config: &Config, doc: &Document) -> Result> { + let mut ctx = tera::Context::new(); + ctx.insert("site", &config.site); + ctx.insert("page_title", &doc.metadata.title); + ctx.insert("page_description", &doc.metadata.description); + ctx.insert("page_author", &doc.metadata.author); + ctx.insert("page_date", &doc.metadata.date); + ctx.insert("page_tags", &doc.metadata.tags); + ctx.insert("page_content", &doc.body); + ctx.insert("page_url", &doc.url); + ctx.insert("page_depth", &doc.depth); + ctx.insert("page_type", &doc.content_type); + ctx.insert("page_is_list", &doc.is_list); + ctx.insert("page_list_items", &doc.list_items); + Ok(Box::new(ctx)) + } +} From 78d33e24dfd2acb3d7925a214b62d9ce000488bf Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:06 +0700 Subject: [PATCH 15/20] feat(compiler): define Generator trait Add Generator trait with a generate method that receives a Pipeline reference and returns Result. This allows post-processing generators to be plugged into the pipeline. --- librawssg_compiler/src/generator.rs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 librawssg_compiler/src/generator.rs diff --git a/librawssg_compiler/src/generator.rs b/librawssg_compiler/src/generator.rs new file mode 100644 index 0000000..8d7578d --- /dev/null +++ b/librawssg_compiler/src/generator.rs @@ -0,0 +1,6 @@ +use crate::pipeline::Pipeline; +use librawssg_error::Result; + +pub trait Generator: Send + Sync { + fn generate(&self, pipeline: &Pipeline) -> Result<()>; +} From 7124ae62ea2a820855553b0061132855d28fb77c Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:06 +0700 Subject: [PATCH 16/20] feat(compiler): add glob pattern matching utility Implement match_pattern function and helper functions for matching path segments against glob patterns, including `**` and `*` wildcards. This is used for content type detection. --- librawssg_compiler/src/pattern.rs | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 librawssg_compiler/src/pattern.rs diff --git a/librawssg_compiler/src/pattern.rs b/librawssg_compiler/src/pattern.rs new file mode 100644 index 0000000..0362a16 --- /dev/null +++ b/librawssg_compiler/src/pattern.rs @@ -0,0 +1,68 @@ +use std::path::Path; + +#[must_use] +pub fn match_pattern(pattern: &str, path: &Path) -> bool { + let path_str = path.to_string_lossy(); + let segments: Vec<&str> = path_str.split('/').collect(); + let pattern_segments: Vec<&str> = pattern.split('/').collect(); + match_pattern_slice(&pattern_segments, &segments) +} + +fn match_pattern_slice(pattern: &[&str], segments: &[&str]) -> bool { + if pattern.is_empty() { + return segments.is_empty(); + } + if segments.is_empty() { + return pattern.iter().all(|&p| p == "**"); + } + + match pattern[0] { + "**" => { + if pattern.len() == 1 { + return true; + } + for i in 0..segments.len() { + if match_pattern_slice(&pattern[1..], &segments[i..]) { + return true; + } + } + false + } + pat => { + if !segment_matches(pat, segments[0]) { + return false; + } + match_pattern_slice(&pattern[1..], &segments[1..]) + } + } +} + +fn segment_matches(pattern: &str, segment: &str) -> bool { + let mut pattern_chars = pattern.chars(); + let mut segment_chars = segment.chars(); + + loop { + match pattern_chars.next() { + Some('*') => { + let rest_of_pattern: String = pattern_chars.clone().collect(); + if rest_of_pattern.is_empty() { + return true; + } + let mut remaining_segment: String = segment_chars.clone().collect(); + while !remaining_segment.is_empty() { + if segment_matches(&rest_of_pattern, &remaining_segment) { + return true; + } + segment_chars.next(); + remaining_segment = segment_chars.clone().collect(); + } + return false; + } + Some(pc) => match segment_chars.next() { + Some(sc) if pc == sc => continue, + _ => return false, + }, + None => return segment_chars.next().is_none(), + } + } +} From 60a2537f903ddb14f4ab2b9d44ade4bdd71e5597 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:30:06 +0700 Subject: [PATCH 17/20] feat(compiler): implement Pipeline Add Pipeline struct with config, fs, renderer, processors, context builder, generators, content/output directories. Implement run method that generates to a temporary directory and atomically renames. Implement document processing, rendering, output writing, and static asset copying. --- librawssg_compiler/src/pipeline.rs | 216 +++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 librawssg_compiler/src/pipeline.rs diff --git a/librawssg_compiler/src/pipeline.rs b/librawssg_compiler/src/pipeline.rs new file mode 100644 index 0000000..1bef3ad --- /dev/null +++ b/librawssg_compiler/src/pipeline.rs @@ -0,0 +1,216 @@ +use crate::ContextBuilder; +use crate::pattern::match_pattern; +use librawssg_config::Config; +use librawssg_error::{Error, Result}; +use librawssg_fs::FileSystem; +use librawssg_handler::Document; +use librawssg_templates::Renderer; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +pub struct Pipeline { + pub(crate) config: Config, + pub(crate) fs: Box, + pub(crate) renderer: Box, + pub(crate) processors: Vec>, + pub(crate) context_builder: Box, + pub(crate) generators: Vec>, + pub(crate) content_dir: PathBuf, + pub(crate) output_dir: PathBuf, +} + +impl Pipeline { + #[must_use] + pub const fn config(&self) -> &Config { + &self.config + } + + pub fn run(&self) -> Result<()> { + let tmp_dir = self.output_dir.with_extension("tmp"); + if self.fs.exists(&tmp_dir) { + self.fs.remove_dir_all(&tmp_dir)?; + } + self.fs.create_dir_all(&tmp_dir)?; + + self.generate_to(&tmp_dir)?; + + if self.fs.exists(&self.output_dir) { + self.fs.remove_dir_all(&self.output_dir)?; + } + match self.fs.rename(&tmp_dir, &self.output_dir) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => { + self.copy_dir_all(&tmp_dir, &self.output_dir)?; + self.fs.remove_dir_all(&tmp_dir)?; + Ok(()) + } + Err(e) => Err(Error::Generation(format!("atomic rename failed: {e}"))), + } + } + + fn generate_to(&self, output_base: &Path) -> Result<()> { + self.fs.create_dir_all(output_base)?; + + let documents = self.process_documents()?; + + let mut docs_by_type: HashMap> = HashMap::new(); + for doc in &documents { + docs_by_type + .entry(doc.content_type.clone()) + .or_default() + .push(doc.clone()); + } + + for doc in &documents { + if doc.is_list { + continue; + } + self.render_document(output_base, doc)?; + } + + for (content_type, docs) in &docs_by_type { + if let Some(rule) = self + .config + .content_rules + .iter() + .find(|r| r.name == *content_type) + { + if rule.list_enabled && !docs.is_empty() { + if let Some(list_template) = &rule.list_template { + let list_doc = Document { + metadata: librawssg_handler::Metadata { + title: content_type.clone(), + ..Default::default() + }, + body: String::new(), + url: format!("{}/index.html", content_type), + output_path: PathBuf::from(format!("{}/index.html", content_type)), + source_path: PathBuf::new(), + depth: 1, + content_type: content_type.clone(), + is_list: true, + list_items: Some(docs.clone()), + taxonomies: HashMap::new(), + }; + self.render_document_with_template(output_base, &list_doc, list_template)?; + } + } + } + } + + if self.fs.exists(Path::new(&self.config.build.static_dir)) { + self.copy_dir_all( + Path::new(&self.config.build.static_dir), + &output_base.join(&self.config.build.static_dir), + )?; + } + + for generator in &self.generators { + generator.generate(self)?; + } + + Ok(()) + } + + fn process_documents(&self) -> Result> { + let mut docs = Vec::new(); + if !self.fs.exists(&self.content_dir) { + return Ok(docs); + } + let files = self.fs.walk_dir(&self.content_dir)?; + for file_path in files { + let rel = file_path + .strip_prefix(&self.content_dir) + .map_err(|e| Error::Generation(e.to_string()))?; + for processor in &self.processors { + if processor.can_process(rel, &file_path) { + if let Some(mut doc) = processor.process(&*self.fs, rel, &self.content_dir)? { + doc.content_type = self.determine_content_type(rel); + doc.depth = rel.components().count().saturating_sub(1); + docs.push(doc); + } + break; + } + } + } + Ok(docs) + } + + fn determine_content_type(&self, rel: &Path) -> String { + for rule in &self.config.content_rules { + if match_pattern(&rule.pattern, rel) { + return rule.name.clone(); + } + } + "page".into() + } + + fn render_document(&self, output_base: &Path, doc: &Document) -> Result<()> { + let template = self.template_for_document(doc)?; + self.render_document_with_template(output_base, doc, &template) + } + + fn render_document_with_template( + &self, + output_base: &Path, + doc: &Document, + template: &str, + ) -> Result<()> { + let ctx = self.context_builder.build_context(&self.config, doc)?; + let html = self.renderer.render(template, &*ctx)?; + self.write_output(output_base, doc, html.as_bytes()) + } + + fn template_for_document(&self, doc: &Document) -> Result { + for rule in &self.config.content_rules { + if rule.name == doc.content_type { + if doc.is_list + && let Some(ref list_template) = rule.list_template + { + return Ok(list_template.clone()); + } + return Ok(rule.template.clone()); + } + } + Err(Error::Generation(format!( + "no content rule found for type '{}'", + doc.content_type + ))) + } + + fn write_output(&self, output_base: &Path, doc: &Document, content: &[u8]) -> Result<()> { + let rel_out = Path::new(&doc.output_path); + if let Some(parent) = rel_out.parent() { + self.fs.create_dir_all(&output_base.join(parent))?; + } + let dest = self + .fs + .safe_join(output_base, rel_out) + .map_err(|e| Error::Generation(format!("unsafe output path: {e}")))?; + self.fs + .write(&dest, content) + .map_err(|e| Error::Generation(format!("write output failed: {e}"))) + } + + fn copy_dir_all(&self, from: &Path, to: &Path) -> Result<()> { + if !self.fs.exists(from) { + return Ok(()); + } + self.fs.create_dir_all(to)?; + for entry in self.fs.walk_dir(from)? { + let rel = entry + .strip_prefix(from) + .map_err(|e| Error::Generation(e.to_string()))?; + let dest = to.join(rel); + if self.fs.is_dir(&entry) { + self.fs.create_dir_all(&dest)?; + } else { + if let Some(parent) = dest.parent() { + self.fs.create_dir_all(parent)?; + } + let _ = self.fs.copy_file(&entry, &dest)?; + } + } + Ok(()) + } +} From b45c797bb7af7ec291e47ffc25eb06f501f57e12 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 02:53:32 +0700 Subject: [PATCH 18/20] feat(compiler): add full compiler tests and refine pipeline (#33) * chore(lock): update lockfile for compiler tera dependency Add tera to librawssg_compiler dependencies in Cargo.lock. This reflects the new direct dependency and keeps the lockfile consistent with Cargo.toml. * feat(compiler): add tera dependency Add tera version 1.20.0 as a direct dependency to librawssg_compiler. This enables TeraContextBuilder without relying on optional feature and simplifies imports. * refactor(builder): fix path comparisons and imports Use `as_path() == Path::new("content")` instead of comparing PathBuf directly. Import `librawssg_handler::Processor` and add missing_debug_implementations attribute to PipelineBuilder. Improve code clarity and avoid type mismatches. * refactor(context): remove feature gate from TeraContextBuilder Derive Debug, Default, Clone, Copy for TeraContextBuilder and remove cfg(feature = "tera") attributes. Tera is now a required dependency, so the feature gate is no longer necessary. * feat(compiler): update Generator trait signature Add `output_base: &Path` parameter to `generate` method. This allows generators to write directly to the output directory during pipeline execution. * refactor(compiler): remove feature gate and add test import Remove `#[cfg(feature = "tera")]` from TeraContextBuilder re-export. Add `#[cfg(test)] use tempfile as _;` to suppress unused import warning in test builds. * refactor(pattern): rewrite glob matching with safer indexing Replace recursive slice indexing with match on pattern and segment options. Avoid potential panics by using get() and first(). Improve readability and maintainability of the pattern matching logic. * refactor(pipeline): simplify list doc creation and static copy Use Document::new and with_list_items to construct list documents instead of manual struct instantiation. Replace repeated path comparisons with let-else block for static directory copying. Add missing_debug_implementations attribute to Pipeline. Reverse content rule iteration to give later rules priority. * feat(handler): add Serialize derive and with_list_items method Derive Serialize for Document to support serialization in templates. Add a `with_list_items` builder method to set the list_items field fluently. This improves API ergonomics and enables future serialization. * test(compiler): add comprehensive full compiler integration tests Add full_compiler_test.rs covering pipeline generation of single pages, content type rules, list pages, static asset copying, generators, atomic replacement, empty content dir, skipped files, and error cases for missing renderer/context builder/invalid config. Uses MockRenderer and RawHtmlProcessor to test end-to-end behavior. --- Cargo.lock | 1 + librawssg_compiler/Cargo.toml | 1 + librawssg_compiler/src/builder.rs | 11 +- librawssg_compiler/src/context.rs | 3 +- librawssg_compiler/src/generator.rs | 3 +- librawssg_compiler/src/lib.rs | 4 +- librawssg_compiler/src/pattern.rs | 77 ++-- librawssg_compiler/src/pipeline.rs | 58 ++- .../tests/full_compiler_test.rs | 433 ++++++++++++++++++ librawssg_handler/src/document.rs | 9 +- 10 files changed, 526 insertions(+), 74 deletions(-) create mode 100644 librawssg_compiler/tests/full_compiler_test.rs diff --git a/Cargo.lock b/Cargo.lock index 47f876e..cbfe261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,6 +343,7 @@ dependencies = [ "librawssg_handler", "librawssg_templates", "tempfile", + "tera", ] [[package]] diff --git a/librawssg_compiler/Cargo.toml b/librawssg_compiler/Cargo.toml index 8bd2882..fa6f5f0 100644 --- a/librawssg_compiler/Cargo.toml +++ b/librawssg_compiler/Cargo.toml @@ -15,6 +15,7 @@ librawssg_fs = { version = "1.0.0", path = "../librawssg_fs" } librawssg_config = { version = "1.0.0", path = "../librawssg_config" } librawssg_handler = { version = "1.0.0", path = "../librawssg_handler" } librawssg_templates = { version = "1.0.0", path = "../librawssg_templates" } +tera = "1.20.0" [dev-dependencies] tempfile = "3" diff --git a/librawssg_compiler/src/builder.rs b/librawssg_compiler/src/builder.rs index bdb7853..0adae23 100644 --- a/librawssg_compiler/src/builder.rs +++ b/librawssg_compiler/src/builder.rs @@ -1,14 +1,15 @@ use super::ContextBuilder; -use super::processor::Processor; use crate::generator::Generator; use crate::pipeline::Pipeline; use librawssg_config::Config; use librawssg_error::Result; use librawssg_fs::FileSystem; use librawssg_fs::RealFs; +use librawssg_handler::Processor; use librawssg_templates::Renderer; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +#[allow(missing_debug_implementations)] pub struct PipelineBuilder { config: Config, content_dir: PathBuf, @@ -41,7 +42,7 @@ impl PipelineBuilder { self } - pub fn load_config + Send + Sync>(mut self, path: P) -> Result { + pub fn load_config + Send + Sync>(mut self, path: P) -> Result { let content = std::fs::read_to_string(path.as_ref()) .map_err(|e| librawssg_error::Error::Config(e.to_string()))?; self.config = Config::from_yaml_str(&content)?; @@ -93,10 +94,10 @@ impl PipelineBuilder { pub fn build(mut self) -> Result { self.config.validate()?; - if self.content_dir == PathBuf::from("content") { + if self.content_dir.as_path() == Path::new("content") { self.content_dir = PathBuf::from(&self.config.build.content_dir); } - if self.output_dir == PathBuf::from("dist") { + if self.output_dir.as_path() == Path::new("dist") { self.output_dir = PathBuf::from(&self.config.build.output_dir); } diff --git a/librawssg_compiler/src/context.rs b/librawssg_compiler/src/context.rs index c63174b..d15df15 100644 --- a/librawssg_compiler/src/context.rs +++ b/librawssg_compiler/src/context.rs @@ -7,10 +7,9 @@ pub trait ContextBuilder: Send + Sync { fn build_context(&self, config: &Config, doc: &Document) -> Result>; } -#[cfg(feature = "tera")] +#[derive(Debug, Default, Clone, Copy)] pub struct TeraContextBuilder; -#[cfg(feature = "tera")] impl ContextBuilder for TeraContextBuilder { fn build_context(&self, config: &Config, doc: &Document) -> Result> { let mut ctx = tera::Context::new(); diff --git a/librawssg_compiler/src/generator.rs b/librawssg_compiler/src/generator.rs index 8d7578d..5c79349 100644 --- a/librawssg_compiler/src/generator.rs +++ b/librawssg_compiler/src/generator.rs @@ -1,6 +1,7 @@ use crate::pipeline::Pipeline; use librawssg_error::Result; +use std::path::Path; pub trait Generator: Send + Sync { - fn generate(&self, pipeline: &Pipeline) -> Result<()>; + fn generate(&self, pipeline: &Pipeline, output_base: &Path) -> Result<()>; } diff --git a/librawssg_compiler/src/lib.rs b/librawssg_compiler/src/lib.rs index 9f8007d..a2225bd 100644 --- a/librawssg_compiler/src/lib.rs +++ b/librawssg_compiler/src/lib.rs @@ -11,5 +11,7 @@ pub use context::ContextBuilder; pub use generator::Generator; pub use pipeline::Pipeline; -#[cfg(feature = "tera")] pub use context::TeraContextBuilder; + +#[cfg(test)] +use tempfile as _; diff --git a/librawssg_compiler/src/pattern.rs b/librawssg_compiler/src/pattern.rs index 0362a16..45037fd 100644 --- a/librawssg_compiler/src/pattern.rs +++ b/librawssg_compiler/src/pattern.rs @@ -9,60 +9,69 @@ pub fn match_pattern(pattern: &str, path: &Path) -> bool { } fn match_pattern_slice(pattern: &[&str], segments: &[&str]) -> bool { - if pattern.is_empty() { - return segments.is_empty(); - } - if segments.is_empty() { - return pattern.iter().all(|&p| p == "**"); - } - - match pattern[0] { - "**" => { - if pattern.len() == 1 { - return true; - } - for i in 0..segments.len() { - if match_pattern_slice(&pattern[1..], &segments[i..]) { + match (pattern.first(), segments.first()) { + (None, None) => true, + (Some(_), None) => pattern.iter().all(|&p| p == "**"), + (None, Some(_)) => false, + (Some(&first_pat), Some(&first_seg)) => { + if first_pat == "**" { + if pattern.len() == 1 { return true; } + let Some(rest_pattern) = pattern.get(1..) else { + return false; + }; + for i in 0..segments.len() { + let Some(rest_segments) = segments.get(i..) else { + continue; + }; + if match_pattern_slice(rest_pattern, rest_segments) { + return true; + } + } + false + } else if segment_matches(first_pat, first_seg) { + match (pattern.get(1..), segments.get(1..)) { + (Some(next_pattern), Some(next_segments)) => { + match_pattern_slice(next_pattern, next_segments) + } + _ => false, + } + } else { + false } - false - } - pat => { - if !segment_matches(pat, segments[0]) { - return false; - } - match_pattern_slice(&pattern[1..], &segments[1..]) } } } fn segment_matches(pattern: &str, segment: &str) -> bool { - let mut pattern_chars = pattern.chars(); - let mut segment_chars = segment.chars(); + let mut pattern_iter = pattern.chars(); + let mut segment_iter = segment.chars(); loop { - match pattern_chars.next() { + match pattern_iter.next() { Some('*') => { - let rest_of_pattern: String = pattern_chars.clone().collect(); - if rest_of_pattern.is_empty() { + let rest: String = pattern_iter.clone().collect(); + if rest.is_empty() { return true; } - let mut remaining_segment: String = segment_chars.clone().collect(); - while !remaining_segment.is_empty() { - if segment_matches(&rest_of_pattern, &remaining_segment) { + let mut remaining: String = segment_iter.clone().collect(); + while !remaining.is_empty() { + if segment_matches(&rest, &remaining) { return true; } - segment_chars.next(); - remaining_segment = segment_chars.clone().collect(); + if segment_iter.next().is_none() { + break; + } + remaining = segment_iter.clone().collect(); } return false; } - Some(pc) => match segment_chars.next() { - Some(sc) if pc == sc => continue, + Some(pc) => match segment_iter.next() { + Some(sc) if pc == sc => {} _ => return false, }, - None => return segment_chars.next().is_none(), + None => return segment_iter.next().is_none(), } } } diff --git a/librawssg_compiler/src/pipeline.rs b/librawssg_compiler/src/pipeline.rs index 1bef3ad..1235e83 100644 --- a/librawssg_compiler/src/pipeline.rs +++ b/librawssg_compiler/src/pipeline.rs @@ -3,16 +3,17 @@ use crate::pattern::match_pattern; use librawssg_config::Config; use librawssg_error::{Error, Result}; use librawssg_fs::FileSystem; -use librawssg_handler::Document; +use librawssg_handler::{Document, Metadata, Processor}; use librawssg_templates::Renderer; use std::collections::HashMap; use std::path::{Path, PathBuf}; +#[allow(missing_debug_implementations)] pub struct Pipeline { pub(crate) config: Config, pub(crate) fs: Box, pub(crate) renderer: Box, - pub(crate) processors: Vec>, + pub(crate) processors: Vec>, pub(crate) context_builder: Box, pub(crate) generators: Vec>, pub(crate) content_dir: PathBuf, @@ -74,41 +75,38 @@ impl Pipeline { .content_rules .iter() .find(|r| r.name == *content_type) + && rule.list_enabled + && !docs.is_empty() + && let Some(list_template) = &rule.list_template { - if rule.list_enabled && !docs.is_empty() { - if let Some(list_template) = &rule.list_template { - let list_doc = Document { - metadata: librawssg_handler::Metadata { - title: content_type.clone(), - ..Default::default() - }, - body: String::new(), - url: format!("{}/index.html", content_type), - output_path: PathBuf::from(format!("{}/index.html", content_type)), - source_path: PathBuf::new(), - depth: 1, - content_type: content_type.clone(), - is_list: true, - list_items: Some(docs.clone()), - taxonomies: HashMap::new(), - }; - self.render_document_with_template(output_base, &list_doc, list_template)?; - } - } + let metadata = Metadata::new(content_type.clone(), String::new())?; + let list_doc = Document::new( + metadata, + String::new(), + format!("{}/index.html", content_type), + PathBuf::from(format!("{}/index.html", content_type)), + PathBuf::from("__list__"), + 1, + content_type.clone(), + true, + )? + .with_list_items(docs.clone()); + self.render_document_with_template(output_base, &list_doc, list_template)?; } } - if self.fs.exists(Path::new(&self.config.build.static_dir)) { - self.copy_dir_all( - Path::new(&self.config.build.static_dir), - &output_base.join(&self.config.build.static_dir), - )?; + let static_src = Path::new(&self.config.build.static_dir); + if self.fs.exists(static_src) { + let static_name = static_src + .file_name() + .unwrap_or_else(|| std::ffi::OsStr::new("static")); + let static_dest = output_base.join(static_name); + self.copy_dir_all(static_src, &static_dest)?; } for generator in &self.generators { - generator.generate(self)?; + generator.generate(self, output_base)?; } - Ok(()) } @@ -137,7 +135,7 @@ impl Pipeline { } fn determine_content_type(&self, rel: &Path) -> String { - for rule in &self.config.content_rules { + for rule in self.config.content_rules.iter().rev() { if match_pattern(&rule.pattern, rel) { return rule.name.clone(); } diff --git a/librawssg_compiler/tests/full_compiler_test.rs b/librawssg_compiler/tests/full_compiler_test.rs new file mode 100644 index 0000000..3835829 --- /dev/null +++ b/librawssg_compiler/tests/full_compiler_test.rs @@ -0,0 +1,433 @@ +use librawssg_compiler::{ContextBuilder, Generator, Pipeline, PipelineBuilder}; +use librawssg_config::{Config, ContentRule}; +use librawssg_fs::{FileSystem, RealFs}; +use librawssg_handler::{Document, Metadata, Processor}; +use librawssg_templates::{RenderContext, Renderer}; +use std::path::{Path, PathBuf}; +use tempfile as _; +use tera as _; + +macro_rules! must { + ($result:expr, $context:expr) => { + match $result { + Ok(value) => value, + Err(err) => { + eprintln!("{} failed: {}", $context, err); + std::process::exit(1); + } + } + }; +} + +struct MockRenderer; + +impl Renderer for MockRenderer { + fn render( + &self, + template_name: &str, + _context: &dyn RenderContext, + ) -> librawssg_error::Result { + Ok(format!("rendered:{template_name}")) + } +} + +struct MockContext; + +impl RenderContext for MockContext { + fn as_any(&self) -> &dyn core::any::Any { + self + } + fn as_mut_any(&mut self) -> &mut dyn core::any::Any { + self + } +} + +struct MockContextBuilder; + +impl ContextBuilder for MockContextBuilder { + fn build_context( + &self, + _config: &Config, + _doc: &Document, + ) -> librawssg_error::Result> { + Ok(Box::new(MockContext)) + } +} + +struct RawHtmlProcessor; + +impl Processor for RawHtmlProcessor { + fn name(&self) -> &'static str { + "raw-html" + } + + fn can_process(&self, relative_path: &Path, _original_path: &Path) -> bool { + relative_path.extension().is_some_and(|ext| ext == "html") + } + + fn process( + &self, + fs: &dyn FileSystem, + relative_path: &Path, + content_dir: &Path, + ) -> librawssg_error::Result> { + let full_path = content_dir.join(relative_path); + let content = fs.read_to_string(&full_path)?; + let url = relative_path + .with_extension("html") + .to_string_lossy() + .to_string(); + let output_path = PathBuf::from(&url); + let metadata = Metadata::new("Test", "Description")?; + let doc = Document::new( + metadata, + content, + url, + output_path, + relative_path.to_path_buf(), + 0, + "page".to_string(), + false, + )?; + Ok(Some(doc)) + } +} + +struct DummyGenerator; + +impl DummyGenerator { + #[must_use] + const fn new() -> Self { + Self + } +} + +impl Generator for DummyGenerator { + fn generate(&self, _pipeline: &Pipeline, output_base: &Path) -> librawssg_error::Result<()> { + let path = output_base.join("generated.txt"); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| librawssg_error::Error::Generation(e.to_string()))?; + } + std::fs::write(&path, b"generated") + .map_err(|e| librawssg_error::Error::Generation(e.to_string())) + } +} +fn setup_config() -> Config { + let mut config = Config::new().with_site_name("Compiler Test"); + config.add_content_rule(ContentRule::new("page", "**/*.html", "base")); + config +} + +fn build_basic_pipeline( + content_dir: &Path, + output_dir: &Path, + config: Config, +) -> librawssg_error::Result { + PipelineBuilder::new() + .config(config) + .content_dir(content_dir) + .output_dir(output_dir) + .with_fs(Box::new(RealFs)) + .with_renderer(Box::new(MockRenderer)) + .with_context_builder(Box::new(MockContextBuilder)) + .add_processor(Box::new(RawHtmlProcessor)) + .build() +} + +#[test] +fn pipeline_generates_single_page() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + must!( + std::fs::write(content_dir.join("index.html"), "

Home

"), + "write index.html" + ); + + let pipeline = must!( + build_basic_pipeline(&content_dir, &output_dir, setup_config()), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + let output_file = output_dir.join("index.html"); + assert!(output_file.exists()); + let content = must!(std::fs::read_to_string(output_file), "read output"); + assert_eq!(content, "rendered:base"); +} + +#[test] +fn pipeline_respects_content_type_rules() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!( + std::fs::create_dir_all(content_dir.join("blog")), + "create blog dir" + ); + must!( + std::fs::write(content_dir.join("blog/post.html"), "post"), + "write post.html" + ); + must!( + std::fs::write(content_dir.join("index.html"), "index"), + "write index.html" + ); + + let mut config = setup_config(); + config.add_content_rule(ContentRule::new("blog", "blog/**/*.html", "post_template")); + + let pipeline = must!( + build_basic_pipeline(&content_dir, &output_dir, config), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + let blog_output = output_dir.join("blog/post.html"); + assert!(blog_output.exists()); + let blog_content = must!(std::fs::read_to_string(blog_output), "read blog output"); + assert_eq!(blog_content, "rendered:post_template"); + + let index_output = output_dir.join("index.html"); + assert!(index_output.exists()); + let index_content = must!(std::fs::read_to_string(index_output), "read index output"); + assert_eq!(index_content, "rendered:base"); +} + +#[test] +fn pipeline_generates_list_pages() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!( + std::fs::create_dir_all(content_dir.join("blog")), + "create blog dir" + ); + must!( + std::fs::write(content_dir.join("blog/one.html"), "one"), + "write one.html" + ); + must!( + std::fs::write(content_dir.join("blog/two.html"), "two"), + "write two.html" + ); + + let mut config = setup_config(); + let mut blog_rule = ContentRule::new("blog", "blog/**/*.html", "post"); + blog_rule.list_template = Some("list".into()); + blog_rule.list_enabled = true; + config.add_content_rule(blog_rule); + + let pipeline = must!( + build_basic_pipeline(&content_dir, &output_dir, config), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + let list_output = output_dir.join("blog/index.html"); + assert!(list_output.exists()); + let list_content = must!(std::fs::read_to_string(list_output), "read list output"); + assert_eq!(list_content, "rendered:list"); +} + +#[test] +fn pipeline_copies_static_assets() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + let static_dir = tmp.path().join("static"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + must!(std::fs::create_dir_all(&static_dir), "create static dir"); + must!( + std::fs::write(content_dir.join("index.html"), "home"), + "write index.html" + ); + must!( + std::fs::write(static_dir.join("style.css"), "body {}"), + "write style.css" + ); + + let mut config = setup_config(); + config.build.static_dir = static_dir.to_string_lossy().to_string(); + + let pipeline = must!( + build_basic_pipeline(&content_dir, &output_dir, config), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + let style_output = output_dir.join(static_dir).join("style.css"); + assert!(style_output.exists()); + let style_content = must!(std::fs::read_to_string(style_output), "read style"); + assert_eq!(style_content, "body {}"); +} + +#[test] +fn pipeline_runs_generators() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + must!( + std::fs::write(content_dir.join("index.html"), "hello"), + "write index.html" + ); + + let generator = DummyGenerator::new(); + let pipeline = must!( + PipelineBuilder::new() + .config(setup_config()) + .content_dir(&content_dir) + .output_dir(&output_dir) + .with_fs(Box::new(RealFs)) + .with_renderer(Box::new(MockRenderer)) + .with_context_builder(Box::new(MockContextBuilder)) + .add_processor(Box::new(RawHtmlProcessor)) + .add_generator(Box::new(generator)) + .build(), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + assert!(output_dir.join("generated.txt").exists()); +} + +#[test] +fn pipeline_atomic_replace_existing_output() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + must!( + std::fs::write(content_dir.join("index.html"), "new"), + "write index.html" + ); + + must!(std::fs::create_dir_all(&output_dir), "create output dir"); + must!( + std::fs::write(output_dir.join("old.txt"), "old"), + "write old file" + ); + + let pipeline = must!( + build_basic_pipeline(&content_dir, &output_dir, setup_config()), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + assert!(!output_dir.join("old.txt").exists()); + assert!(output_dir.join("index.html").exists()); + let content = must!( + std::fs::read_to_string(output_dir.join("index.html")), + "read new" + ); + assert_eq!(content, "rendered:base"); +} + +#[test] +fn pipeline_handles_empty_content_dir() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + + let pipeline = must!( + build_basic_pipeline(&content_dir, &output_dir, setup_config()), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + assert!(output_dir.exists()); + let entries = must!(std::fs::read_dir(&output_dir), "read output dir"); + assert_eq!(entries.count(), 0); +} + +#[test] +fn pipeline_skips_files_not_matching_processor() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + must!( + std::fs::write(content_dir.join("index.html"), "home"), + "write index.html" + ); + must!( + std::fs::write(content_dir.join("script.js"), "console.log('hi')"), + "write script.js" + ); + + let pipeline = must!( + build_basic_pipeline(&content_dir, &output_dir, setup_config()), + "build pipeline" + ); + must!(pipeline.run(), "run pipeline"); + + assert!(output_dir.join("index.html").exists()); + assert!(!output_dir.join("script.js").exists()); +} + +#[test] +fn pipeline_errors_when_renderer_missing() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + + let result = PipelineBuilder::new() + .config(setup_config()) + .content_dir(&content_dir) + .output_dir(&output_dir) + .with_fs(Box::new(RealFs)) + .with_context_builder(Box::new(MockContextBuilder)) + .add_processor(Box::new(RawHtmlProcessor)) + .build(); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!(err, librawssg_error::Error::Config(_))); + } +} + +#[test] +fn pipeline_errors_when_context_builder_missing() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + must!(std::fs::create_dir_all(&content_dir), "create content dir"); + + let result = PipelineBuilder::new() + .config(setup_config()) + .content_dir(&content_dir) + .output_dir(&output_dir) + .with_fs(Box::new(RealFs)) + .with_renderer(Box::new(MockRenderer)) + .add_processor(Box::new(RawHtmlProcessor)) + .build(); + assert!(result.is_err()); + if let Err(err) = result { + assert!(matches!(err, librawssg_error::Error::Config(_))); + } +} + +#[test] +fn pipeline_errors_on_invalid_config() { + let tmp = must!(tempfile::TempDir::new(), "TempDir::new"); + let content_dir = tmp.path().join("content"); + let output_dir = tmp.path().join("dist"); + + let mut config = setup_config(); + config.site.site_name = " ".to_string(); + + let result = PipelineBuilder::new() + .config(config) + .content_dir(&content_dir) + .output_dir(&output_dir) + .with_fs(Box::new(RealFs)) + .with_renderer(Box::new(MockRenderer)) + .with_context_builder(Box::new(MockContextBuilder)) + .add_processor(Box::new(RawHtmlProcessor)) + .build(); + assert!(result.is_err()); +} diff --git a/librawssg_handler/src/document.rs b/librawssg_handler/src/document.rs index b9cad17..645bfa8 100644 --- a/librawssg_handler/src/document.rs +++ b/librawssg_handler/src/document.rs @@ -1,9 +1,10 @@ use crate::Metadata; use librawssg_error::{Error, Result}; +use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[non_exhaustive] pub struct Document { pub metadata: Metadata, @@ -78,4 +79,10 @@ impl Document { pub const fn depth(&self) -> usize { self.depth } + + #[must_use] + pub fn with_list_items(mut self, items: Vec) -> Self { + self.list_items = Some(items); + self + } } From c868488f341fc25b37b81d5d948898090f5cf6e7 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 03:02:21 +0700 Subject: [PATCH 19/20] feat(workspace): add librawssg and demo crates (#34) * chore: update .gitignore for demo output Add an additional `/dist` entry to ignore the generated demo site output. This prevents accidental commits of build artifacts. * chore: update lockfile for librawssg and demo crates Add entries for the new `librawssg` and `librawssg_demo` crates and their workspace dependencies. This keeps the lockfile synchronized with Cargo.toml. * feat(workspace): add librawssg and librawssg_demo to members Include the new `librawssg` placeholder crate and the `librawssg_demo` application in the workspace member list. This enables building and testing them alongside existing crates. * feat(librawssg): add placeholder crate manifest Create Cargo.toml for the top-level `librawssg` crate. This crate currently serves as a placeholder for future public API re-exports or convenience features. * feat(librawssg): add placeholder library Add a minimal lib.rs with a simple `add` function and a test. This bootstraps the top-level crate and allows workspace compilation. * feat(demo): add demo crate manifest Create Cargo.toml for `librawssg_demo` application. Declare dependencies on all librawssg workspace crates and set metadata. * feat(demo): add about page raw content Add a simple HTML about page that demonstrates how raw content files are processed by the demo pipeline. The content is static and will be rendered into a full page. * feat(demo): add index page raw content Add a welcome page with sample HTML. This file is processed by RawFileProcessor and demonstrates generation from a `.raw` source file. * feat(demo): add demo application entry point Implement main.rs with a custom RawFileProcessor and pipeline setup. Load Tera templates, configure content rules, and run the pipeline to generate a static site from `src/content`. * feat(demo): add static stylesheet Add a basic CSS file that styles the demo site. It is copied verbatim to the output directory by the pipeline's static asset handling. * feat(demo): add base Tera template Add a base HTML template that uses Tera placeholders for page title and content. It demonstrates how the compiler populates context values during rendering. --- .gitignore | 3 +- Cargo.lock | 16 +++++ Cargo.toml | 2 +- librawssg/Cargo.toml | 9 +++ librawssg/src/lib.rs | 14 ++++ librawssg_demo/Cargo.toml | 21 ++++++ librawssg_demo/src/content/about.raw | 2 + librawssg_demo/src/content/index.raw | 2 + librawssg_demo/src/main.rs | 95 ++++++++++++++++++++++++++ librawssg_demo/src/static/style.css | 9 +++ librawssg_demo/src/templates/base.tera | 18 +++++ 11 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 librawssg/Cargo.toml create mode 100644 librawssg/src/lib.rs create mode 100644 librawssg_demo/Cargo.toml create mode 100644 librawssg_demo/src/content/about.raw create mode 100644 librawssg_demo/src/content/index.raw create mode 100644 librawssg_demo/src/main.rs create mode 100644 librawssg_demo/src/static/style.css create mode 100644 librawssg_demo/src/templates/base.tera diff --git a/.gitignore b/.gitignore index 1468f0e..b265701 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .velodiff_dist.diff .snapcat.md /dist -pull_request_body.md \ No newline at end of file +pull_request_body.md +/dist \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index cbfe261..28bd408 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,10 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "librawssg" +version = "0.1.0" + [[package]] name = "librawssg_compiler" version = "1.0.0" @@ -357,6 +361,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "librawssg_demo" +version = "0.1.0" +dependencies = [ + "librawssg_compiler", + "librawssg_config", + "librawssg_error", + "librawssg_fs", + "librawssg_handler", + "librawssg_templates", +] + [[package]] name = "librawssg_error" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 9fcbab3..b4fa58a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["librawssg_compiler", "librawssg_config", "librawssg_error", "librawssg_fs","librawssg_handler", "librawssg_templates"] +members = ["librawssg","librawssg_compiler", "librawssg_config", "librawssg_demo", "librawssg_error", "librawssg_fs","librawssg_handler", "librawssg_templates"] [workspace.lints.clippy] all = { level = "deny", priority = -1 } diff --git a/librawssg/Cargo.toml b/librawssg/Cargo.toml new file mode 100644 index 0000000..fb8a976 --- /dev/null +++ b/librawssg/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "librawssg" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lints] +workspace = true diff --git a/librawssg/src/lib.rs b/librawssg/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/librawssg/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/librawssg_demo/Cargo.toml b/librawssg_demo/Cargo.toml new file mode 100644 index 0000000..1091b01 --- /dev/null +++ b/librawssg_demo/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "librawssg_demo" +version = "0.1.0" +edition = "2024" +description = "Demo application for librawssg" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "demo", "librawssg"] +categories = ["development-tools::build-utils"] + +[dependencies] +librawssg_error = { version = "1.0.0", path = "../librawssg_error" } +librawssg_compiler = { version = "1.0.0", path = "../librawssg_compiler" } +librawssg_config = { version = "1.0.0", path = "../librawssg_config" } +librawssg_fs = { version = "1.0.0", path = "../librawssg_fs" } +librawssg_handler = { version = "1.0.0", path = "../librawssg_handler" } +librawssg_templates = { version = "1.0.0", path = "../librawssg_templates" } + +[lints] +workspace = true diff --git a/librawssg_demo/src/content/about.raw b/librawssg_demo/src/content/about.raw new file mode 100644 index 0000000..77c0bed --- /dev/null +++ b/librawssg_demo/src/content/about.raw @@ -0,0 +1,2 @@ +

About

+

Learn more about this project.

diff --git a/librawssg_demo/src/content/index.raw b/librawssg_demo/src/content/index.raw new file mode 100644 index 0000000..7e723d2 --- /dev/null +++ b/librawssg_demo/src/content/index.raw @@ -0,0 +1,2 @@ +

Welcome to the Demo

+

This page was generated from a .raw file.

diff --git a/librawssg_demo/src/main.rs b/librawssg_demo/src/main.rs new file mode 100644 index 0000000..87ab3f7 --- /dev/null +++ b/librawssg_demo/src/main.rs @@ -0,0 +1,95 @@ +#![allow(clippy::multiple_crate_versions)] + +use librawssg_compiler::{PipelineBuilder, TeraContextBuilder}; +use librawssg_config::{Config, ContentRule}; +use librawssg_fs::{FileSystem, RealFs}; +use librawssg_handler::{Document, Metadata, Processor}; +use librawssg_templates::TeraRenderer; +use std::path::{Path, PathBuf}; + +struct RawFileProcessor; + +impl Processor for RawFileProcessor { + fn name(&self) -> &'static str { + "raw-file" + } + + fn can_process(&self, relative_path: &Path, _original_path: &Path) -> bool { + relative_path.extension().is_some_and(|ext| ext == "raw") + } + + fn process( + &self, + fs: &dyn FileSystem, + relative_path: &Path, + content_dir: &Path, + ) -> librawssg_error::Result> { + let full_path = content_dir.join(relative_path); + let body = fs.read_to_string(&full_path)?; + + let stem = relative_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("untitled"); + let title = stem + .split('-') + .map(|word| { + let mut c = word.chars(); + c.next().map_or_else(String::new, |first| { + first.to_uppercase().collect::() + c.as_str() + }) + }) + .collect::>() + .join(" "); + + let metadata = Metadata::new(title, String::new())?; + let url = relative_path + .with_extension("html") + .to_string_lossy() + .to_string(); + let output_path = PathBuf::from(&url); + + let doc = Document::new( + metadata, + body, + url, + output_path, + relative_path.to_path_buf(), + relative_path.components().count().saturating_sub(1), + "page".to_string(), + false, + )?; + Ok(Some(doc)) + } +} + +fn main() -> Result<(), Box> { + let base = Path::new(env!("CARGO_MANIFEST_DIR")); + let content_dir = base.join("src/content"); + let templates_dir = base.join("src/templates"); + let static_dir = base.join("src/static"); + let output_dir = base.join("dist"); + let mut renderer = TeraRenderer::new(); + renderer.load_templates_dir(&templates_dir)?; + + let mut config = Config::new().with_site_name("Demo Site"); + config.add_content_rule(ContentRule::new("page", "**/*.raw", "base.tera")); + config.build.content_dir = content_dir.to_string_lossy().to_string(); + config.build.output_dir = output_dir.to_string_lossy().to_string(); + config.build.static_dir = static_dir.to_string_lossy().to_string(); + + let pipeline = PipelineBuilder::new() + .config(config) + .content_dir(&content_dir) + .output_dir(&output_dir) + .with_fs(Box::new(RealFs)) + .with_renderer(Box::new(renderer)) + .with_context_builder(Box::new(TeraContextBuilder)) + .add_processor(Box::new(RawFileProcessor)) + .build()?; + + pipeline.run()?; + + println!("✅ Site generated in '{}'", output_dir.display()); + Ok(()) +} diff --git a/librawssg_demo/src/static/style.css b/librawssg_demo/src/static/style.css new file mode 100644 index 0000000..458e2aa --- /dev/null +++ b/librawssg_demo/src/static/style.css @@ -0,0 +1,9 @@ +body { + font-family: sans-serif; + max-width: 800px; + margin: 0 auto; + padding: 2rem; +} +nav a { + margin-right: 1rem; +} diff --git a/librawssg_demo/src/templates/base.tera b/librawssg_demo/src/templates/base.tera new file mode 100644 index 0000000..66f5de5 --- /dev/null +++ b/librawssg_demo/src/templates/base.tera @@ -0,0 +1,18 @@ + + + + + + {{ page_title }} + + + + +
+ {{ page_content | safe }} +
+ + From 91064d297ea37c53b4976251d3864b8fba7d0465 Mon Sep 17 00:00:00 2001 From: mroczect Date: Tue, 8 Sep 2026 03:05:45 +0700 Subject: [PATCH 20/20] feat(librawssg): add facade crate with re-exports (#35) * chore(lock): update Cargo.lock for librawssg facade crate Update librawssg entry to version 1.0.0 and add dependencies on all workspace crates: compiler, config, error, fs, handler, templates. This reflects the new facade crate's Cargo.toml changes. * feat(librawssg): turn crate into facade with dependencies Bump version to 1.0.0 and add path dependencies to all internal workspace crates. Add package metadata including description, license, repository, readme, keywords, and categories. This enables the crate to re-export the entire library. * feat(librawssg): re-export all workspace crates and types Replace placeholder `add` function with module re-exports for config, fs, handler, templates, compiler, and error. Re-export common types like Config, Document, PipelineBuilder, TeraRenderer, and Error. Add `#![allow(clippy::multiple_crate_versions)]` to suppress workspace lint warnings. * docs(librawssg): add README for facade crate Add a short README describing the facade crate and listing the re-exported building blocks. This improves crate documentation and discoverability. --- Cargo.lock | 10 +++++++++- librawssg/Cargo.toml | 14 +++++++++++++- librawssg/README.md | 12 ++++++++++++ librawssg/src/lib.rs | 39 +++++++++++++++++++++++++++------------ 4 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 librawssg/README.md diff --git a/Cargo.lock b/Cargo.lock index 28bd408..4e5a8bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,7 +335,15 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "librawssg" -version = "0.1.0" +version = "1.0.0" +dependencies = [ + "librawssg_compiler", + "librawssg_config", + "librawssg_error", + "librawssg_fs", + "librawssg_handler", + "librawssg_templates", +] [[package]] name = "librawssg_compiler" diff --git a/librawssg/Cargo.toml b/librawssg/Cargo.toml index fb8a976..1e6e861 100644 --- a/librawssg/Cargo.toml +++ b/librawssg/Cargo.toml @@ -1,9 +1,21 @@ [package] name = "librawssg" -version = "0.1.0" +version = "1.0.0" edition = "2024" +description = "A modular static site generator library" +license = "MIT" +repository = "https://github.com/mroczect/librawssg" +readme = "README.md" +keywords = ["ssg", "static-site-generator", "compiler", "templates"] +categories = ["development-tools::build-utils"] [dependencies] +librawssg_error = { version = "1.0.0", path = "../librawssg_error" } +librawssg_fs = { version = "1.0.0", path = "../librawssg_fs" } +librawssg_config = { version = "1.0.0", path = "../librawssg_config" } +librawssg_handler = { version = "1.0.0", path = "../librawssg_handler" } +librawssg_templates = { version = "1.0.0", path = "../librawssg_templates" } +librawssg_compiler = { version = "1.0.0", path = "../librawssg_compiler" } [lints] workspace = true diff --git a/librawssg/README.md b/librawssg/README.md new file mode 100644 index 0000000..6efdc79 --- /dev/null +++ b/librawssg/README.md @@ -0,0 +1,12 @@ +# librawssg + +A modular static site generator library for Rust. + +This facade crate re-exports the essential building blocks: +- Configuration types +- Filesystem abstraction +- Content processor contracts +- Template rendering (Tera built-in) +- Build pipeline orchestration + +See the crate documentation for usage examples. diff --git a/librawssg/src/lib.rs b/librawssg/src/lib.rs index b93cf3f..095386a 100644 --- a/librawssg/src/lib.rs +++ b/librawssg/src/lib.rs @@ -1,14 +1,29 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; +#![allow(clippy::multiple_crate_versions)] - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +pub mod config { + pub use librawssg_config::*; +} +pub mod fs { + pub use librawssg_fs::*; +} +pub mod handler { + pub use librawssg_handler::*; } +pub mod templates { + pub use librawssg_templates::*; +} +pub mod compiler { + pub use librawssg_compiler::*; +} +pub mod error { + pub use librawssg_error::*; +} + +pub use librawssg_compiler::{ + ContextBuilder, Generator, Pipeline, PipelineBuilder, TeraContextBuilder, +}; +pub use librawssg_config::{BuildConfig, Config, ContentRule, NavItem, SiteConfig}; +pub use librawssg_error::{Error, Result}; +pub use librawssg_fs::{FileSystem, RealFs}; +pub use librawssg_handler::{Document, Metadata, Processor}; +pub use librawssg_templates::{RenderContext, Renderer, TeraRenderer};