From 1de8682dad021f8cfad9285f43d73cd633ceb83e Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 17 Apr 2025 10:51:28 +0200 Subject: [PATCH 1/4] Move tooling into crates/ directory This seems a little cleaner, especially if more tooling is added in the form of little one-off crates. --- .github/workflows/snapshot_tests.yml | 4 ++-- .gitignore | 2 +- Cargo.toml | 2 +- README.md | 6 +++--- {front_matter => crates/front_matter}/Cargo.toml | 0 {front_matter => crates/front_matter}/src/lib.rs | 2 +- {snapshot => crates/snapshot}/Cargo.toml | 0 {snapshot => crates/snapshot}/src/lib.rs | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) rename {front_matter => crates/front_matter}/Cargo.toml (100%) rename {front_matter => crates/front_matter}/src/lib.rs (99%) rename {snapshot => crates/snapshot}/Cargo.toml (100%) rename {snapshot => crates/snapshot}/src/lib.rs (95%) diff --git a/.github/workflows/snapshot_tests.yml b/.github/workflows/snapshot_tests.yml index f412a2ce3..7061875f4 100644 --- a/.github/workflows/snapshot_tests.yml +++ b/.github/workflows/snapshot_tests.yml @@ -20,6 +20,6 @@ jobs: - run: git fetch --depth 2 - run: git checkout origin/master - name: Generate good snapshots - run: INSTA_OUTPUT=none INSTA_UPDATE=always cargo test -- --include-ignored + run: INSTA_OUTPUT=none INSTA_UPDATE=always cargo test -p snapshot -- --include-ignored - run: git checkout $GITHUB_SHA # merge of master+branch - - run: cargo test -- --include-ignored + - run: INSTA_OUTPUT=none INSTA_UPDATE=no cargo test -p snapshot -- --include-ignored diff --git a/.gitignore b/.gitignore index c97e6b3e0..d0cd697ad 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ /static/styles/fonts.css /static/styles/noscript.css -/snapshot/src/snapshots +/crates/snapshot/src/snapshots diff --git a/Cargo.toml b/Cargo.toml index 4650619f6..5f1f17f28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["front_matter", "snapshot"] +members = ["crates/front_matter", "crates/snapshot"] diff --git a/README.md b/README.md index bc2241fea..95a7c5f11 100644 --- a/README.md +++ b/README.md @@ -65,16 +65,16 @@ You can also run these tests locally for a faster feedback cycle: - Generate the good snapshots to compare against, usually based off the master branch: ```sh - cargo insta test --accept --include-ignored + cargo insta test -p snapshot --accept --include-ignored ``` Consider making a commit with these snapshots, so you can always check the diff of your changes with git: ```sh - git add --force snapshot/src/snapshots # snapshots are ignored by default + git add --force crates/snapshot/src/snapshots # snapshots are ignored by default git commit --message "WIP add good snapshots" ``` Since we can't merge the snapshots to main, don't forget to drop this commit when opening a pull request. - Compare the output of the branch you're working on with the good snapshots: ```sh - cargo insta test --review --include-ignored + cargo insta test -p snapshot --review --include-ignored ``` diff --git a/front_matter/Cargo.toml b/crates/front_matter/Cargo.toml similarity index 100% rename from front_matter/Cargo.toml rename to crates/front_matter/Cargo.toml diff --git a/front_matter/src/lib.rs b/crates/front_matter/src/lib.rs similarity index 99% rename from front_matter/src/lib.rs rename to crates/front_matter/src/lib.rs index aba00fd10..67bb17220 100644 --- a/front_matter/src/lib.rs +++ b/crates/front_matter/src/lib.rs @@ -255,7 +255,7 @@ The post {post} has abnormal front matter. } fn all_posts() -> impl Iterator { - walkdir::WalkDir::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../content")) + walkdir::WalkDir::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../content")) .into_iter() .filter_map(|e| e.ok().map(|e| e.into_path())) .filter(|p| { diff --git a/snapshot/Cargo.toml b/crates/snapshot/Cargo.toml similarity index 100% rename from snapshot/Cargo.toml rename to crates/snapshot/Cargo.toml diff --git a/snapshot/src/lib.rs b/crates/snapshot/src/lib.rs similarity index 95% rename from snapshot/src/lib.rs rename to crates/snapshot/src/lib.rs index ccca9eb55..44a78be29 100644 --- a/snapshot/src/lib.rs +++ b/crates/snapshot/src/lib.rs @@ -1,6 +1,6 @@ #[test] fn snapshot() { - std::env::set_current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/..")).unwrap(); + std::env::set_current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")).unwrap(); let _ = std::fs::remove_dir_all("public"); let status = std::process::Command::new("zola") .arg("build") @@ -11,7 +11,7 @@ fn snapshot() { let timestamped_files = ["releases.json", "feed.xml"]; let inexplicably_non_deterministic_files = ["2023/08/07/Rust-Survey-2023-Results/experiences.png"]; - insta::glob!("../..", "public/**/*", |path| { + insta::glob!("../../..", "public/**/*", |path| { if path.is_dir() { return; } From 384b4ff2beb6e0cdb4ef7de349ed325422a1052b Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 18 Apr 2025 17:14:12 +0200 Subject: [PATCH 2/4] Document deprecated post description I presume the templates where changed at some point to not use the post description anymore, but I haven't actually looked when and why that happened. --- crates/front_matter/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/front_matter/src/lib.rs b/crates/front_matter/src/lib.rs index 67bb17220..db169a1a3 100644 --- a/crates/front_matter/src/lib.rs +++ b/crates/front_matter/src/lib.rs @@ -27,6 +27,8 @@ pub struct FrontMatter { pub author: Option, #[serde(default)] pub authors: Vec, + /// Deprecated. Post descriptions are not used anywhere in the templates. + /// (only section descriptions) pub description: Option, /// Used for `releases/X.XX.X` redirects and ones from the old URL scheme to /// preserve permalinks (e.g. slug.html => slug/). From 7c8c7f2114035eb2ad48b29bc3d82b14299af6c7 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Thu, 17 Apr 2025 11:05:13 +0200 Subject: [PATCH 3/4] front_matter: skip serializing empty aliases --- crates/front_matter/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/front_matter/src/lib.rs b/crates/front_matter/src/lib.rs index db169a1a3..396ccb2d8 100644 --- a/crates/front_matter/src/lib.rs +++ b/crates/front_matter/src/lib.rs @@ -32,7 +32,7 @@ pub struct FrontMatter { pub description: Option, /// Used for `releases/X.XX.X` redirects and ones from the old URL scheme to /// preserve permalinks (e.g. slug.html => slug/). - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub aliases: Vec, /// Moved to the `extra` table. #[serde(default, skip_serializing)] From 8d5ab1ab73a869d371dea33762db2cf148faf350 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 18 Apr 2025 22:33:41 +0200 Subject: [PATCH 4/4] Add blog post generator This makes it easier for blog authors to get started without having to figure out the front matter format first. All they have to do is run `cargo blog`. The generator asks all relevant questions to generate valid and complete front matter. --- .cargo/config.toml | 2 + Cargo.lock | 347 ++++++++++++++++++++++++++++++- Cargo.toml | 2 +- README.md | 28 +-- crates/front_matter/src/lib.rs | 7 +- crates/generate_blog/Cargo.toml | 10 + crates/generate_blog/src/main.rs | 221 ++++++++++++++++++++ 7 files changed, 582 insertions(+), 35 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 crates/generate_blog/Cargo.toml create mode 100644 crates/generate_blog/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..a9b0b23cc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +blog = ["run", "--package", "generate_blog"] diff --git a/Cargo.lock b/Cargo.lock index fb89ce2e0..b7e83d09b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + [[package]] name = "bstr" version = "1.12.0" @@ -21,6 +39,18 @@ dependencies = [ "serde", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "console" version = "0.15.11" @@ -30,9 +60,40 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", ] +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -65,6 +126,34 @@ dependencies = [ "walkdir", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generate_blog" +version = "0.1.0" +dependencies = [ + "front_matter", + "inquire", + "serde", + "toml", +] + [[package]] name = "globset" version = "0.4.16" @@ -100,6 +189,23 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.9.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "insta" version = "1.42.2" @@ -128,6 +234,16 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -140,12 +256,56 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -184,6 +344,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "regex" version = "1.11.1" @@ -222,6 +391,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -251,12 +426,48 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + [[package]] name = "snapshot" version = "0.1.0" @@ -275,6 +486,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "toml" version = "0.8.20" @@ -315,6 +536,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "walkdir" version = "2.5.0" @@ -325,13 +558,50 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", ] [[package]] @@ -340,7 +610,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -349,28 +634,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -383,24 +686,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 5f1f17f28..a534b4374 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["crates/front_matter", "crates/snapshot"] +members = ["crates/front_matter", "crates/generate_blog", "crates/snapshot"] diff --git a/README.md b/README.md index 95a7c5f11..a3c310d8c 100644 --- a/README.md +++ b/README.md @@ -28,29 +28,11 @@ Code of Conduct, see `CODE_OF_CONDUCT.md` for more. ### Writing a new blog post -If you want to include images in your post, please store them in the repository. -You can store your main blog post in `content//index.md`. -Images go into the same directory: `content//my_image.png`. -Now you can reference that image with a simple relative path: `![alt text](my_image.png)`. - -A post's date of publication is embedded in the `path` key of the front matter. -Unless the exact date is known in advance, keep the placeholder (`9999/12/31`) until the post is about to be published. -Don't worry, there's a CI check to prevent a post with a placeholder date from being deployed. - -Here is an example of the front matter format: -```md -+++ -path = "9999/12/31/some-slug" -title = "Title of the blog post" -authors = ["Blog post author (or on behalf of which team)"] -description = "(optional)" -aliases = ["releases/X.XX.X"] # only if the post is a release - -[extra] # optional section -team = "Team Name" # if post is made on behalf of a team -team_url = "https://www.rust-lang.org/governance/teams/..." # required if team is set -release = true # (to be only used for official posts about Rust releases announcements) -+++ +There is an interactive blog post generator that takes care of some boilerplate for you. +To use it, run: + +``` +cargo blog ``` ### Snapshot testing diff --git a/crates/front_matter/src/lib.rs b/crates/front_matter/src/lib.rs index 396ccb2d8..f456c61ed 100644 --- a/crates/front_matter/src/lib.rs +++ b/crates/front_matter/src/lib.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use toml::value::Date; /// The front matter of a markdown blog post. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct FrontMatter { /// Deprecated. The plan was probably to have more specialized templates /// at some point. That didn't materialize, all posts are rendered with the @@ -139,6 +139,11 @@ pub fn normalize( bail!("extra.team and extra.team_url must always come in a pair"); } + // the crate generate_blog may create this placeholder + if front_matter.aliases.iter().any(|a| a == "releases/?.??.?") { + bail!("invalid release alias: releases/?.??.?"); + } + if front_matter.extra.release && !front_matter.aliases.iter().any(|a| a.contains("releases")) { // Make sure release posts have a matching `releases/X.XX.X` alias. let version = guess_version_from_path(&front_matter.path).unwrap_or("?.??.?".into()); diff --git a/crates/generate_blog/Cargo.toml b/crates/generate_blog/Cargo.toml new file mode 100644 index 000000000..b5dc67b80 --- /dev/null +++ b/crates/generate_blog/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "generate_blog" +version = "0.1.0" +edition = "2024" + +[dependencies] +front_matter = { version = "0.1.0", path = "../front_matter" } +inquire = "0.7.5" +serde = "1.0.219" +toml = "0.8.20" diff --git a/crates/generate_blog/src/main.rs b/crates/generate_blog/src/main.rs new file mode 100644 index 000000000..19e517b8e --- /dev/null +++ b/crates/generate_blog/src/main.rs @@ -0,0 +1,221 @@ +use std::{error::Error, fmt::Display, fs, path::PathBuf, process::Command}; + +use front_matter::FrontMatter; +use inquire::{Confirm, Select, Text, validator::Validation}; + +fn main() -> Result<(), Box> { + println!("\nHi, thanks for writing a post for the Rust blog!\n"); + + let title = Text::new("What's the title of your post?") + .with_validator(|input: &str| { + if input.is_empty() { + return Ok(Validation::Invalid("Title cannot be empty".into())); + } + Ok(Validation::Valid) + }) + .prompt()?; + + let slug = title + .to_lowercase() + .replace(' ', "-") + .chars() + .filter(|c| c.is_ascii_alphanumeric() || ['-', '.'].contains(c)) + .collect::(); + let slug = slug.trim_matches('-').to_string(); + + let blog = Select::new( + "Which blog should the post belong to?", + vec![Blog::Main, Blog::InsideRust], + ) + .prompt()?; + let blog_path = blog.path(); + + let release = if blog == Blog::Main { + Confirm::new("Is this an official Rust release announcement post?") + .with_default(false) + .prompt()? + } else { + false + }; + + let aliases = if !release { + vec![] + } else { + // parse version from title (the front matter tests check against ?.??.?) + let version = try_parse_version_from_title(&title).unwrap_or_else(|| "?.??.?".into()); + vec![format!("releases/{version}")] + }; + + let author = if release { + "The Rust Release Team".into() + } else { + let author_prompt = Text::new("Who's the author of the post?").with_help_message( + "If the post is authored by a team as a whole, write the team name here", + ); + if let Some(git_user) = guess_author_from_git() { + author_prompt.with_initial_value(git_user.trim()).prompt()? + } else { + author_prompt.prompt()? + } + }; + + let (team, team_url) = 'team_prompt: { + if release { + // For official release annoucement posts, the whole + // "Rust Release Team" is usually the author. + break 'team_prompt (None, None); + } + if !Confirm::new("Is the post made on behalf of a team?") + .with_help_message( + "If the main author is already a team instead of an individual, answer No.", + ) + .prompt()? + { + break 'team_prompt (None, None); + } + let team = Text::new("What is the team?").prompt()?; + let url = Text::new("At what URL can people find the team?") + .with_initial_value("https://www.rust-lang.org/governance/teams/") + .prompt()?; + (Some(team), Some(url)) + }; + + let front_matter = FrontMatter { + path: format!("{blog_path}9999/12/31/{slug}"), + title, + authors: vec![author], + aliases, + extra: front_matter::Extra { + team, + team_url, + release, + }, + ..Default::default() + }; + + let contents = format!( + "+++\n{front_matter}+++\n{MARKDOWN_BOILERPLATE}", + front_matter = toml::to_string_pretty(&front_matter).unwrap(), + ); + + let colocate = Confirm::new("Are you planning to include images in the post?") + .with_help_message( + " + To include images in a post, the post will be stored in /index.md, + instead of the usualy .md. Images can then be stored in the directory + / right next to the post itself. They can be included with a relative + link, e.g. ![alt text](my_impage.png). +", + ) + .with_default(false) + .prompt()?; + + let base_path = format!("content/{blog_path}{slug}"); + + let path_ending = if colocate { "/index.md" } else { ".md" }; + + let mut path = PathBuf::from(format!("{base_path}{path_ending}")); + + 'try_write_file: { + if fs::create_dir_all(path.parent().unwrap()).is_ok() + && !path.exists() + && fs::write(&path, &contents).is_ok() + { + break 'try_write_file; + } + // A blog with that slug already exists. Generate an unambiguous path. + for i in 2.. { + path = PathBuf::from(format!("{base_path}@{i}{path_ending}")); + if fs::create_dir_all(path.parent().unwrap()).is_ok() + && !path.exists() + && fs::write(&path, &contents).is_ok() + { + break 'try_write_file; + } + } + } + + let path = path.display(); + println!( + " +Success! A new blog post has been generated at the following path: + +{path} + +Remember: A post's date of publication is embedded in the `path` key of the +front matter. Keep the generated placeholder (9999/12/31) until the exact date +of publication is known. This is to prevent a post with an incorrect date from +being published - CI checks against the placeholder. +" + ); + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +enum Blog { + #[default] + Main, + InsideRust, +} + +impl Blog { + fn path(&self) -> &'static str { + match self { + Blog::Main => "", + Blog::InsideRust => "inside-rust/", + } + } +} + +impl Display for Blog { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Blog::Main => write!(f, "The main Rust blog"), + Blog::InsideRust => write!(f, r#"The "Inside Rust" blog"#), + } + } +} + +fn try_parse_version_from_title(title: &str) -> Option { + // assuming there won't ever be a Rust 2.0 + let (_, rest) = title.split_once("1.")?; + let (minor, rest) = rest.split_once('.')?; + let patch = rest + .get(0..1) + .and_then(|c| c.parse::().ok()) + .unwrap_or_default(); + Some(format!("1.{minor}.{patch}")) +} + +fn guess_author_from_git() -> Option { + String::from_utf8( + Command::new("git") + .args(["config", "get", "user.name"]) + .output() + .ok()? + .stdout, + ) + .ok() +} + +const MARKDOWN_BOILERPLATE: &str = " +**insert your content here** + +If you're wondering about available Markdown features, the blog is rendered with [Zola]. +Here's an excerpt from its documentation: + +> Content is written in [CommonMark], a strongly defined, highly compatible specification of [Markdown]. +> Zola uses [pulldown-cmark] to parse markdown files. +> The goal of this library is 100% compliance with the CommonMark spec. +> It adds a few additional features such as parsing [footnotes], Github flavored [tables], Github flavored [task lists] and [strikethrough]. + +[Zola]: https://www.getzola.org/documentation/getting-started/overview/ +[CommonMark]: https://commonmark.org/ +[Markdown]: https://www.markdownguide.org/ +[pulldown-cmark]: https://github.com/raphlinus/pulldown-cmark#pulldown-cmark +[footnotes]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes +[tables]: https://github.github.com/gfm/#tables-extension- +[task lists]: https://github.github.com/gfm/#task-list-items-extension- +[strikethrough]: https://github.github.com/gfm/#strikethrough-extension- +";