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/.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.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 4650619f6..a534b4374 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["front_matter", "snapshot"] +members = ["crates/front_matter", "crates/generate_blog", "crates/snapshot"] diff --git a/README.md b/README.md index bc2241fea..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 @@ -65,16 +47,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 95% rename from front_matter/src/lib.rs rename to crates/front_matter/src/lib.rs index aba00fd10..f456c61ed 100644 --- a/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 @@ -27,10 +27,12 @@ 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/). - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub aliases: Vec, /// Moved to the `extra` table. #[serde(default, skip_serializing)] @@ -137,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()); @@ -255,7 +262,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/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- +"; 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; }