├── .gitignore ├── src ├── lib.rs ├── main.rs ├── html.rs ├── text.rs └── site.rs ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── Cargo.toml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .cargo 3 | Makefile* 4 | Make.zsh 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod html; 2 | mod site; 3 | mod text; 4 | 5 | pub use crate::site::*; 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Hayato Ito "] 3 | description = "A simple, fast, opinionated static site generator" 4 | edition = "2024" 5 | license = "Apache-2.0" 6 | name = "site" 7 | readme = "README.md" 8 | repository = "https://github.com/hayatoito/site" 9 | version = "2.0.1" 10 | 11 | [dependencies] 12 | anyhow = "1.0.100" 13 | clap = { version = "4", features = ["derive"] } 14 | env_logger = "0.11.8" 15 | glob = "0.3.3" 16 | log = "0.4.28" 17 | minijinja = { version = "2.12.0", features = ["loader"] } 18 | pulldown-cmark = "0.13.0" 19 | rayon = "1.11.0" 20 | regex = "1.11.2" 21 | serde = { version = "1.0.226", features = ["derive"] } 22 | toml = "0.9.7" 23 | unicode-width = "0.2.1" 24 | walkdir = "2.5.0" 25 | 26 | [dependencies.chrono] 27 | features = ["serde"] 28 | version = "0.4.42" 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest, macOS-latest, windows-latest] 8 | runs-on: ${{ matrix.os }} 9 | steps: 10 | - uses: actions/checkout@v5 11 | - run: rustup update stable 12 | - uses: actions/cache@v4 13 | with: 14 | path: | 15 | ~/.cargo/bin/ 16 | ~/.cargo/registry/index/ 17 | ~/.cargo/registry/cache/ 18 | ~/.cargo/git/db/ 19 | target/ 20 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 21 | - run: rustup component add rustfmt 22 | - run: rustup component add clippy 23 | - run: cargo build 24 | - run: cargo test 25 | - run: cargo fmt --all -- --check 26 | - run: cargo clippy --all-targets --all-features -- --deny warnings 27 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use regex::Regex; 3 | use std::path::PathBuf; 4 | 5 | use site::{Config, Result, Site}; 6 | 7 | #[derive(Parser, Debug)] 8 | struct Cli { 9 | #[command(subcommand)] 10 | cmd: Command, 11 | } 12 | 13 | #[derive(Parser, Debug)] 14 | #[command(version, about)] 15 | enum Command { 16 | Build { 17 | #[structopt(long = "root", default_value = ".")] 18 | root: String, 19 | #[structopt(long = "config")] 20 | config: Option, 21 | #[structopt(long = "out")] 22 | out: String, 23 | #[structopt(long = "article-regex")] 24 | article_regex: Option, 25 | }, 26 | } 27 | 28 | fn main() -> Result<()> { 29 | let opt = Cli::parse(); 30 | env_logger::init(); 31 | match opt.cmd { 32 | Command::Build { 33 | config, 34 | root, 35 | out, 36 | article_regex, 37 | } => { 38 | let root = PathBuf::from(root); 39 | let config = { 40 | let mut default_config = Config::read(root.join("config.toml"))?; 41 | if let Some(config) = config.as_ref() { 42 | default_config.extend(&mut Config::read(config)?); 43 | } 44 | default_config 45 | }; 46 | let app = Site::new( 47 | config, 48 | root, 49 | PathBuf::from(out), 50 | article_regex.map(|regex| Regex::new(®ex).expect("invalid regex")), 51 | ); 52 | app.build() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*' 6 | jobs: 7 | build: 8 | env: 9 | BIN_NAME: site 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macOS-latest, windows-latest] 13 | include: 14 | - os: ubuntu-latest 15 | target: x86_64-unknown-linux-musl 16 | - os: windows-latest 17 | target: x86_64-pc-windows-msvc 18 | - os: macOS-latest 19 | target: x86_64-apple-darwin 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v5 23 | - run: rustup update stable 24 | - run: rustup target add ${{ matrix.target }} 25 | - run: cargo build --release --target ${{ matrix.target }} 26 | - run: cargo test --release --target ${{ matrix.target }} 27 | - name: Package 28 | if: matrix.os != 'windows-latest' 29 | run: | 30 | strip target/${{ matrix.target }}/release/${{ env.BIN_NAME }} 31 | cd target/${{ matrix.target }}/release 32 | tar czvf ../../../${{ env.BIN_NAME }}-${{ matrix.target }}.tar.gz ${{ env.BIN_NAME }} 33 | cd - 34 | - name: Package (windows) 35 | if: matrix.os == 'windows-latest' 36 | run: | 37 | strip target/${{ matrix.target }}/release/${{ env.BIN_NAME }}.exe 38 | cd target/${{ matrix.target }}/release 39 | 7z a ../../../${{ env.BIN_NAME }}-${{ matrix.target }}.zip ${{ env.BIN_NAME }}.exe 40 | cd - 41 | - name: Release 42 | uses: softprops/action-gh-release@v2 43 | with: 44 | files: '${{ env.BIN_NAME }}-${{ matrix.target }}*' 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /src/html.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::collections::HashMap; 3 | use std::sync::LazyLock; 4 | 5 | fn id_from_title(title: &str) -> String { 6 | let mut title = title.to_string(); 7 | 8 | // Skip html-encoded stuff 9 | const REPL_SUB: &[&str] = &["<", ">", "&", "'", """]; 10 | for sub in REPL_SUB { 11 | title = title.replace(sub, " "); 12 | } 13 | 14 | // Convert the given string to a valid HTML element ID 15 | let ret = title 16 | .chars() 17 | .map(|ch| { 18 | if ch.is_ascii_alphanumeric() { 19 | ch.to_ascii_lowercase() 20 | } else { 21 | ' ' 22 | } 23 | }) 24 | .collect::(); 25 | 26 | static SPACES: LazyLock = LazyLock::new(|| Regex::new(r" +").unwrap()); 27 | 28 | let ret = SPACES.replace_all(ret.trim(), "-").to_string(); 29 | if ret.is_empty() { "a".to_string() } else { ret } 30 | } 31 | 32 | pub fn build_header_links(html: &str) -> String { 33 | let header = Regex::new(r#"\d)( id="(?P.*?)")?>(?P.*?)</h\d>"#).unwrap(); 34 | let mut id_counter = HashMap::new(); 35 | 36 | header 37 | .replace_all(html, |caps: ®ex::Captures<'_>| { 38 | let level = caps 39 | .name("level") 40 | .unwrap() 41 | .as_str() 42 | .parse() 43 | .expect("Regex should ensure we only ever get numbers here"); 44 | let title = caps.name("title").unwrap().as_str(); 45 | let id = caps.name("id").map(|id| id.as_str()); 46 | 47 | wrap_header_with_link(level, title, id, &mut id_counter) 48 | }) 49 | .into_owned() 50 | } 51 | 52 | fn wrap_header_with_link( 53 | level: usize, 54 | title: &str, 55 | id: Option<&str>, 56 | id_counter: &mut HashMap<String, usize>, 57 | ) -> String { 58 | if let Some(id) = id { 59 | let id_count = id_counter.entry(id.to_owned()).or_insert(0); 60 | if *id_count != 0 { 61 | log::warn!("Found duplicated id: {title} {id}"); 62 | } 63 | *id_count += 1; 64 | format!(r##"<h{level} id="{id}"><a class="self-link" href="#{id}">{title}</a></h{level}>"##,) 65 | } else { 66 | let id = id_from_title(title); 67 | let id_count = id_counter.entry(id.to_owned()).or_insert(0); 68 | 69 | let id = if *id_count == 0 { 70 | id 71 | } else { 72 | format!("{id}-{}", *id_count) 73 | }; 74 | *id_count += 1; 75 | format!(r##"<h{level} id="{id}"><a class="self-link" href="#{id}">{title}</a></h{level}>"##,) 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | 83 | #[test] 84 | fn find_headert() { 85 | let header = 86 | Regex::new(r#"<h(?P<level>\d)( id="(?P<id>.*?)")?>(?P<title>.*?)</h\d>"#).unwrap(); 87 | 88 | let cap = header.captures(r#"<h1 id="id1">title1</h1>"#).unwrap(); 89 | assert_eq!(cap.name("level").unwrap().as_str(), "1"); 90 | assert_eq!(cap.name("id").unwrap().as_str(), "id1"); 91 | assert_eq!(cap.name("title").unwrap().as_str(), "title1"); 92 | 93 | let cap = header.captures(r#"<h1>title1</h1>"#).unwrap(); 94 | assert_eq!(cap.name("level").unwrap().as_str(), "1"); 95 | assert!(cap.name("id").is_none()); 96 | assert_eq!(cap.name("title").unwrap().as_str(), "title1"); 97 | } 98 | 99 | #[test] 100 | fn build_header_links_test() { 101 | let html = r#" 102 | <h1 id="id1">title1</h1> 103 | <h2>title2</h2> 104 | <h3>title2</h3> 105 | "#; 106 | let replaced = build_header_links(html); 107 | assert_eq!( 108 | replaced, 109 | r##" 110 | <h1 id="id1"><a class="self-link" href="#id1">title1</a></h1> 111 | <h2 id="title2"><a class="self-link" href="#title2">title2</a></h2> 112 | <h3 id="title2-1"><a class="self-link" href="#title2-1">title2</a></h3> 113 | "## 114 | ); 115 | } 116 | 117 | #[test] 118 | fn id_from_content_test() { 119 | assert_eq!(id_from_title("abc"), "abc"); 120 | assert_eq!(id_from_title(" abc "), "abc"); 121 | assert_eq!(id_from_title("abc def"), "abc-def"); 122 | assert_eq!(id_from_title("あいう abc えお def"), "abc-def"); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | /// For pretieer: wrapping: "proseWrap": "always" 2 | /// e.g. "あいう\nえお" -> "あいうえお" 3 | /// See the test. 4 | pub fn remove_newline_between_cjk(s: &str) -> String { 5 | use unicode_width::UnicodeWidthChar; 6 | 7 | enum State { 8 | Char, 9 | WideChar, 10 | WideCharNewlineSpaces, 11 | } 12 | 13 | enum CharacterType { 14 | Newline, 15 | Space, 16 | WideChar, 17 | Char, 18 | } 19 | 20 | impl CharacterType { 21 | fn from(c: char) -> CharacterType { 22 | match c { 23 | '\n' => CharacterType::Newline, 24 | ' ' => CharacterType::Space, 25 | _ => match c.width() { 26 | Some(w) if w >= 2 => CharacterType::WideChar, 27 | _ => CharacterType::Char, 28 | }, 29 | } 30 | } 31 | } 32 | 33 | let mut out = String::new(); 34 | let mut buffer = String::new(); 35 | 36 | let mut state = State::Char; 37 | for c in s.chars() { 38 | let new_char = CharacterType::from(c); 39 | match (state, new_char) { 40 | (State::Char, CharacterType::Newline | CharacterType::Space | CharacterType::Char) => { 41 | out.push(c); 42 | state = State::Char; 43 | } 44 | (State::Char, CharacterType::WideChar) => { 45 | out.push(c); 46 | state = State::WideChar; 47 | } 48 | (State::WideChar, CharacterType::Newline) => { 49 | buffer.push(c); 50 | state = State::WideCharNewlineSpaces; 51 | } 52 | (State::WideChar, CharacterType::Space | CharacterType::Char) => { 53 | out.push(c); 54 | state = State::Char; 55 | } 56 | (State::WideChar, CharacterType::WideChar) => { 57 | out.push(c); 58 | state = State::WideChar; 59 | } 60 | (State::WideCharNewlineSpaces, CharacterType::Newline | CharacterType::Char) => { 61 | out.push_str(&buffer); 62 | out.push(c); 63 | buffer.clear(); 64 | state = State::Char; 65 | } 66 | (State::WideCharNewlineSpaces, CharacterType::Space) => { 67 | buffer.push(c); 68 | state = State::WideCharNewlineSpaces; 69 | } 70 | (State::WideCharNewlineSpaces, CharacterType::WideChar) => { 71 | // Ignore buffer 72 | buffer.clear(); 73 | out.push(c); 74 | state = State::WideChar; 75 | } 76 | } 77 | } 78 | out 79 | } 80 | 81 | pub fn remove_prettier_ignore_preceeding_code_block(s: &str) -> String { 82 | s.replace("\n<!-- prettier-ignore -->\n```", "\n```") 83 | } 84 | 85 | pub fn remove_deno_fmt_ignore(s: &str) -> String { 86 | s.replace("\n<!-- deno-fmt-ignore -->\n", "\n") 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | #[test] 94 | fn remove_prettier_ignore_preceeding_code_block_test() { 95 | let s = r"foo 96 | <!-- prettier-ignore --> 97 | ```html"; 98 | assert_eq!( 99 | remove_prettier_ignore_preceeding_code_block(s), 100 | "foo\n```html" 101 | ); 102 | 103 | let s = r"foo 104 | 105 | <!-- prettier-ignore --> 106 | ```html"; 107 | assert_eq!( 108 | remove_prettier_ignore_preceeding_code_block(s), 109 | "foo\n\n```html" 110 | ); 111 | } 112 | 113 | #[test] 114 | fn remove_newline_between_cjk_test() { 115 | let s = r"abc 116 | de"; 117 | assert_eq!(remove_newline_between_cjk(s), "abc\nde"); 118 | 119 | let s = r"ä 120 | ä"; 121 | assert_eq!(remove_newline_between_cjk(s), "ä\nä"); 122 | 123 | let s = r"あいう 124 | えお"; 125 | assert_eq!(remove_newline_between_cjk(s), "あいうえお"); 126 | 127 | let s = r"あいう 128 | ab"; 129 | assert_eq!(remove_newline_between_cjk(s), "あいう\nab"); 130 | 131 | let s = r"あいう 132 | ä"; 133 | assert_eq!(remove_newline_between_cjk(s), "あいう\nä"); 134 | 135 | // For itemized list. Remove newline + spaces 136 | let s = r"- あいう 137 | えお"; 138 | assert_eq!(remove_newline_between_cjk(s), "- あいうえお"); 139 | 140 | let s = r"- あいう 141 | ab"; 142 | assert_eq!(remove_newline_between_cjk(s), "- あいう\n ab"); 143 | 144 | // Don't remove. newline + newline 145 | let s = r"あいう 146 | 147 | えお"; 148 | assert_eq!(remove_newline_between_cjk(s), "あいう\n\nえお"); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Site 2 | 3 | [![build](https://github.com/hayatoito/site/workflows/build/badge.svg)](https://github.com/hayatoito/site/actions) 4 | [![crates.io](https://img.shields.io/crates/v/site.svg)](https://crates.io/crates/site) 5 | 6 | **Site** is a fast, simple, and opinionated static site generator written in 7 | [Rust](https://www.rust-lang.org/). Its main features are: 8 | 9 | - **Markdown-based**: Write your content in Markdown with extensions, powered by 10 | [pulldown_cmark](https://crates.io/crates/pulldown-cmark). 11 | - **Jinja2 Templates**: Uses the [minijinja](https://crates.io/crates/minijinja) 12 | engine for templating. 13 | - **Fast & Simple**: Blazingly fast, with parallel processing of articles. 14 | - **Inspired by Pelican**: Borrows the concepts of _Articles_ and _Pages_ from 15 | the Python static site generator 16 | [Pelican](http://docs.getpelican.com/en/stable/). 17 | - **Opinionated by Design**: Implements only what the author needs for a simple 18 | static site like [hayatoito.github.io](https://hayatoito.github.io/). For 19 | custom features, forking is recommended. The codebase is intentionally small, 20 | making it easy to understand and modify. 21 | 22 | # Installation 23 | 24 | ```shell 25 | cargo install site 26 | ``` 27 | 28 | # Usage 29 | 30 | There is no documentation yet. 31 | 32 | In the meantime, you can use the 33 | [hayatoito/hayatoito.github.io](https://github.com/hayatoito/hayatoito.github.io) 34 | repository as a starter template. The author's site, 35 | [hayatoito.github.io](https://hayatoito.github.io/), is built from this 36 | repository. 37 | 38 | # Folder Structure 39 | 40 | ```text 41 | root_dir/ 42 | - src/ 43 | - (Your markdown files go here) 44 | - template/ 45 | - (Your template files go here) 46 | ``` 47 | 48 | - `src/`: This directory contains all your Markdown files. They are converted to 49 | HTML using Jinja2 templates and placed in the output directory. Any other 50 | files in this directory are also copied to the output directory. 51 | 52 | - `template/`: This directory holds your Jinja2 template files. 53 | 54 | # Markdown Format 55 | 56 | `Site` uses Markdown with TOML front matter for metadata. 57 | 58 | ```markdown 59 | # Article title 60 | 61 | <!-- 62 | date = "2021-12-01" 63 | --> 64 | 65 | # Section 66 | 67 | Hello Article! 68 | 69 | - hello 70 | - world 71 | ``` 72 | 73 | # Metadata 74 | 75 | | Name | Description | Default Value | 76 | | ------------- | ------------------------------------------------- | ---------------------------- | 77 | | `page` | If `true`, the file is treated as a page. | `false` | 78 | | `date` | The publication date of the article. | (mandatory for articles) | 79 | | `update_date` | The date the article was last updated. | (none) | 80 | | `author` | The author of the article. | (none) | 81 | | `slug` | The URL slug for the page. | (derived from the file path) | 82 | | `math` | If `true`, enables MathJax for the page. | `false` | 83 | | `draft` | If `true`, the article will not be published. | `false` | 84 | | `template` | The template file to use from the `template` dir. | `article` or `page` | 85 | 86 | # Pages 87 | 88 | If a Markdown file's metadata contains `page = true`, **Site** treats it as a 89 | _page_ instead of an _article_. 90 | 91 | ```markdown 92 | # Page title 93 | 94 | <!-- 95 | page = true 96 | --> 97 | 98 | # Section 99 | 100 | Hello Page! 101 | 102 | - hello 103 | - world 104 | ``` 105 | 106 | The main differences between an article and a page are: 107 | 108 | - A page is not included in the `articles` or `articles_by_year` template 109 | variables. 110 | - A page does not require a `date` in its metadata. 111 | 112 | # Template Variables 113 | 114 | | Name | Available On | Description | 115 | | ------------------ | ---------------- | ------------------------------------------------- | 116 | | `entry` | Pages & Articles | The current article or page object. | 117 | | `site` | Pages & Articles | Site configuration from the `--config` parameter. | 118 | | `articles` | Pages only | A list of all articles. | 119 | | `articles_by_year` | Pages only | A list of articles grouped by year. | 120 | 121 | An article template does not have access to other articles. 122 | 123 | ## `entry` 124 | 125 | The `entry` object contains all the metadata fields, plus the following: 126 | 127 | | Name | Description | 128 | | --------------- | ----------------------- | 129 | | `entry.title` | The title. | 130 | | `entry.content` | The rendered HTML body. | 131 | 132 | # Generate Your Site 133 | 134 | ## From the CLI 135 | 136 | ```shell 137 | site build --root . --out out --config=config.toml 138 | ``` 139 | 140 | The `root` directory must contain `src` and `template` directories. For more 141 | examples, see the 142 | [Make.zsh](https://github.com/hayatoito/hayatoito.github.io/blob/main/Make.zsh) 143 | file in the starter template. 144 | 145 | ## With GitHub Actions 146 | 147 | You can use GitHub Actions to automatically build and deploy your site to GitHub 148 | Pages. See the example 149 | [build.yml](https://github.com/hayatoito/hayatoito.github.io/blob/main/.github/workflows/build.yml) 150 | workflow file. 151 | -------------------------------------------------------------------------------- /src/site.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | pub use anyhow::Result; 3 | use anyhow::{Error, anyhow}; 4 | use chrono::Datelike; 5 | use minijinja::{Environment, Value, context, path_loader}; 6 | use rayon::prelude::*; 7 | use regex::Regex; 8 | use serde::{Deserialize, Serialize}; 9 | use std::collections::BTreeMap; 10 | use std::path::{Path, PathBuf}; 11 | use std::str::FromStr; 12 | use std::sync::LazyLock; 13 | 14 | use crate::html; 15 | use crate::text; 16 | 17 | #[derive(PartialEq, Eq, Debug, Deserialize, Default)] 18 | struct Metadata { 19 | page: Option<bool>, 20 | title: String, 21 | author: Option<String>, 22 | date: Option<chrono::NaiveDate>, 23 | update_date: Option<chrono::NaiveDate>, 24 | slug: Option<String>, 25 | math: Option<bool>, 26 | draft: Option<bool>, 27 | template: Option<String>, 28 | } 29 | 30 | impl FromStr for Metadata { 31 | type Err = Error; 32 | 33 | fn from_str(s: &str) -> Result<Self> { 34 | Ok(toml::from_str(s)?) 35 | } 36 | } 37 | 38 | #[derive(Debug)] 39 | struct MarkdownFile { 40 | relative_path: PathBuf, 41 | markdown: Markdown, 42 | } 43 | 44 | #[derive(PartialEq, Eq, Debug)] 45 | struct Markdown { 46 | metadata: Metadata, 47 | content: String, 48 | } 49 | 50 | impl Markdown { 51 | pub fn render(&self) -> String { 52 | let mut opts = pulldown_cmark::Options::empty(); 53 | opts.insert(pulldown_cmark::Options::ENABLE_TABLES); 54 | opts.insert(pulldown_cmark::Options::ENABLE_FOOTNOTES); 55 | opts.insert(pulldown_cmark::Options::ENABLE_STRIKETHROUGH); 56 | opts.insert(pulldown_cmark::Options::ENABLE_TASKLISTS); 57 | opts.insert(pulldown_cmark::Options::ENABLE_HEADING_ATTRIBUTES); 58 | opts.insert(pulldown_cmark::Options::ENABLE_GFM); 59 | let mut html = String::with_capacity(self.content.len() * 3 / 2); 60 | let content = self.pre_process_content(); 61 | let p = pulldown_cmark::Parser::new_ext(&content, opts); 62 | pulldown_cmark::html::push_html(&mut html, p); 63 | Self::post_process_markdown_html(&html) 64 | } 65 | 66 | fn pre_process_content(&self) -> String { 67 | let s = text::remove_newline_between_cjk(&self.content); 68 | let s = text::remove_prettier_ignore_preceeding_code_block(&s); 69 | text::remove_deno_fmt_ignore(&s) 70 | } 71 | 72 | fn post_process_markdown_html(html: &str) -> String { 73 | let html = html::build_header_links(html); 74 | html.to_string() 75 | } 76 | } 77 | 78 | impl FromStr for Markdown { 79 | type Err = Error; 80 | 81 | fn from_str(s: &str) -> Result<Markdown> { 82 | // Skip the comment at the beginning. Emacs may use the first line for buffer-local variables. 83 | // e.g. <!-- -*- apheleia-formatters: prettier -*- --> 84 | static COMMENT_LINES: LazyLock<Regex> = 85 | LazyLock::new(|| Regex::new(r"^<!--.*-->\n+").unwrap()); 86 | 87 | static TITLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^# +(.+?) *\n+").unwrap()); 88 | 89 | let s = COMMENT_LINES.replace_all(s, ""); 90 | 91 | let (metadata_yaml, content) = match TITLE.captures(&s) { 92 | Some(cap) => { 93 | // If the first line starts with "#", treat it as a title. 94 | let title = cap[1].to_string(); 95 | let s = TITLE.replace(&s, "").to_string(); 96 | 97 | let mut split = s.splitn(2, "\n\n"); 98 | 99 | // Add "title: xxx" to metadata 100 | let metadata_yaml = split.next().ok_or_else(|| anyhow!("split error"))?; 101 | // TODO: Espace double quote? 102 | let metadata_yaml = format!("title = \"{title}\"\n{metadata_yaml}"); 103 | 104 | let content = split.next().unwrap_or(""); 105 | 106 | (metadata_yaml, content.to_string()) 107 | } 108 | _ => { 109 | let mut split = s.splitn(2, "\n\n"); 110 | let metadata_yaml = split.next().ok_or_else(|| anyhow!("split error"))?; 111 | let content = split.next().unwrap_or(""); 112 | (metadata_yaml.to_string(), content.to_string()) 113 | } 114 | }; 115 | 116 | // Ignore comments, such as <!-- prettier-ignore -->, in metadata. 117 | static METADATA_COMMENT: LazyLock<Regex> = 118 | LazyLock::new(|| Regex::new(r"(<!--.*\n*)|(-->.*\n*)").unwrap()); 119 | 120 | let metadata_yaml = METADATA_COMMENT.replace_all(&metadata_yaml, ""); 121 | assert!(!metadata_yaml.contains("-->")); 122 | assert!(!metadata_yaml.contains("<!--")); 123 | 124 | Ok(Markdown { 125 | metadata: metadata_yaml 126 | .parse() 127 | .with_context(|| format!("can not parse metatada: {metadata_yaml}"))?, 128 | content, 129 | }) 130 | } 131 | } 132 | 133 | fn slug_to_url(slug: &str) -> String { 134 | if slug.is_empty() || slug == "index" { 135 | "".to_string() 136 | } else if slug.ends_with('/') { 137 | slug.to_string() 138 | } else if Path::new(slug).extension().is_none() { 139 | format!("{slug}/") 140 | } else { 141 | slug.to_string() 142 | } 143 | } 144 | 145 | fn url_to_filename(url: &str) -> String { 146 | if url.is_empty() || url.ends_with('/') { 147 | format!("{}{}", url, "index.html") 148 | } else { 149 | url.to_string() 150 | } 151 | } 152 | 153 | #[derive(PartialEq, Eq, Debug, Serialize, Default)] 154 | struct Article { 155 | title: String, 156 | slug: String, 157 | author: Option<String>, 158 | date: Option<chrono::NaiveDate>, 159 | update_date: Option<chrono::NaiveDate>, 160 | draft: bool, 161 | url: String, 162 | page: bool, 163 | math: bool, 164 | template: Option<String>, 165 | content: String, 166 | } 167 | 168 | impl Article { 169 | fn new( 170 | MarkdownFile { 171 | relative_path, 172 | markdown, 173 | }: MarkdownFile, 174 | ) -> Article { 175 | log::debug!("article: {}", relative_path.display()); 176 | let slug = if let Some(slug) = markdown.metadata.slug.as_ref() { 177 | slug.to_string() 178 | } else { 179 | relative_path 180 | .file_stem() 181 | .unwrap() 182 | .to_str() 183 | .unwrap() 184 | .to_string() 185 | }; 186 | let url = relative_path 187 | .parent() 188 | .unwrap() 189 | .join(slug_to_url(&slug)) 190 | .display() 191 | .to_string(); 192 | let content = markdown.render(); 193 | 194 | Article { 195 | title: markdown.metadata.title, 196 | slug, 197 | author: markdown.metadata.author, 198 | date: markdown.metadata.date, 199 | update_date: markdown.metadata.update_date, 200 | draft: markdown.metadata.draft.unwrap_or(false), 201 | url, 202 | page: markdown.metadata.page.unwrap_or(false), 203 | math: markdown.metadata.math.unwrap_or(false), 204 | template: markdown.metadata.template, 205 | content, 206 | } 207 | } 208 | 209 | fn context(&self, config: &Config, articles: Option<&[Article]>) -> Value { 210 | #[derive(PartialEq, Eq, Debug, Default, Serialize)] 211 | struct YearArticles<'a> { 212 | year: i32, 213 | articles: Vec<&'a Article>, 214 | } 215 | 216 | let mut context = config.context(); 217 | if let Some(articles) = articles { 218 | let mut articles_by_year = BTreeMap::<i32, Vec<&Article>>::new(); 219 | for a in articles { 220 | articles_by_year 221 | .entry(a.date.as_ref().unwrap().year()) 222 | .or_default() 223 | .push(a); 224 | } 225 | let mut articles_by_year = articles_by_year 226 | .into_iter() 227 | .map(|(year, mut articles)| { 228 | articles.sort_by_key(|a| a.date); 229 | articles.reverse(); 230 | YearArticles { year, articles } 231 | }) 232 | .collect::<Vec<_>>(); 233 | articles_by_year.reverse(); 234 | 235 | context = context! { 236 | articles, 237 | articles_by_year, 238 | ..context 239 | }; 240 | }; 241 | context = context! { 242 | entry => &self, 243 | ..context 244 | }; 245 | context 246 | } 247 | 248 | fn template_name(&self) -> &str { 249 | match self.template.as_ref() { 250 | Some(a) => a, 251 | None => { 252 | if self.page { 253 | "page" 254 | } else { 255 | "article" 256 | } 257 | } 258 | } 259 | } 260 | 261 | fn render( 262 | &self, 263 | config: &Config, 264 | articles: Option<&[Article]>, 265 | env: &Environment, 266 | ) -> Result<String> { 267 | let context = self.context(config, articles); 268 | let template = env.get_template(&format!("{}.jinja", self.template_name()))?; 269 | template 270 | .render(&context) 271 | .map_err(|e| anyhow!("renderer err: {}", e)) 272 | } 273 | 274 | fn render_and_write( 275 | &self, 276 | config: &Config, 277 | articles: Option<&[Article]>, 278 | env: &Environment, 279 | out_dir: &Path, 280 | ) -> Result<()> { 281 | let html = self.render(config, articles, env)?; 282 | let mut out_file = PathBuf::from(out_dir); 283 | out_file.push(url_to_filename(&self.url)); 284 | log::debug!("{:32} => {}", self.url, out_file.display()); 285 | std::fs::create_dir_all(out_file.parent().unwrap())?; 286 | std::fs::write(&out_file, html)?; 287 | Ok(()) 288 | } 289 | } 290 | 291 | pub struct Config(std::collections::BTreeMap<String, String>); 292 | 293 | impl Config { 294 | pub fn read(path: impl AsRef<Path>) -> Result<Config> { 295 | let s = std::fs::read_to_string(path.as_ref())?; 296 | Ok(Config(toml::from_str(&s)?)) 297 | } 298 | 299 | fn context(&self) -> minijinja::Value { 300 | context! { site => &self.0} 301 | } 302 | 303 | pub fn extend(&mut self, config: &mut Config) { 304 | self.0.append(&mut config.0); 305 | } 306 | } 307 | 308 | pub struct Site { 309 | config: Config, 310 | root_dir: PathBuf, 311 | src_dir: PathBuf, 312 | out_dir: PathBuf, 313 | article_regex: Option<Regex>, 314 | } 315 | 316 | impl Site { 317 | pub fn new( 318 | config: Config, 319 | root_dir: PathBuf, 320 | out_dir: PathBuf, 321 | article_regex: Option<Regex>, 322 | ) -> Site { 323 | let src_dir = root_dir.join("src"); 324 | Site { 325 | config, 326 | root_dir: root_dir.canonicalize().unwrap(), 327 | src_dir, 328 | out_dir, 329 | article_regex, 330 | } 331 | } 332 | 333 | pub fn build(&self) -> Result<()> { 334 | let src_dir = self.root_dir.join("src"); 335 | let template_dir = self.root_dir.join("template"); 336 | 337 | let mut env = Environment::new(); 338 | env.set_loader(path_loader(template_dir)); 339 | env.set_auto_escape_callback(|_name| minijinja::AutoEscape::None); 340 | env.set_keep_trailing_newline(true); 341 | 342 | self.render_markdowns(&env, src_dir)?; 343 | if self.article_regex.is_none() { 344 | self.copy_files()?; 345 | } 346 | Ok(()) 347 | } 348 | 349 | fn collect_markdown(&self, src_dir: impl AsRef<Path>) -> Result<Vec<MarkdownFile>> { 350 | glob::glob(&format!("{}/**/*.md", src_dir.as_ref().display()))? 351 | .filter_map(std::result::Result::ok) 352 | .flat_map(|f| match self.article_regex { 353 | Some(ref regex) => { 354 | if regex.is_match(f.as_os_str().to_str().unwrap()) { 355 | Some(f) 356 | } else { 357 | None 358 | } 359 | } 360 | _ => Some(f), 361 | }) 362 | .map(|f| -> Result<MarkdownFile> { 363 | let relative_path = f.strip_prefix(&src_dir).expect("prefix does not match"); 364 | log::debug!("found: {}", relative_path.display()); 365 | Ok(MarkdownFile { 366 | relative_path: PathBuf::from(relative_path), 367 | markdown: std::fs::read_to_string(&f) 368 | .with_context(|| format!("can not read: {}", f.display()))? 369 | .parse() 370 | .with_context(|| format!("can not parse: {}", f.display()))?, 371 | }) 372 | }) 373 | .collect::<Vec<Result<MarkdownFile>>>() 374 | .into_iter() 375 | .collect() 376 | } 377 | 378 | fn render_markdowns(&self, env: &Environment, src_dir: impl AsRef<Path>) -> Result<()> { 379 | let src_dir = src_dir.as_ref().canonicalize().unwrap(); 380 | log::info!("Collecting markdown: {}", src_dir.display()); 381 | let (pages, articles) = self 382 | .collect_markdown(&src_dir)? 383 | .into_iter() 384 | .partition::<Vec<MarkdownFile>, _>(|src| src.markdown.metadata.page.unwrap_or(false)); 385 | log::info!( 386 | "Found {} articles and {} pages", 387 | articles.len(), 388 | pages.len() 389 | ); 390 | 391 | for article in &articles { 392 | anyhow::ensure!( 393 | article.markdown.metadata.date.is_some(), 394 | "{} doesn't have date", 395 | article.relative_path.display() 396 | ) 397 | } 398 | 399 | log::info!("Build articles"); 400 | let mut articles = articles 401 | .into_par_iter() 402 | .map(|m| -> Result<Article> { 403 | let article = Article::new(m); 404 | article.render_and_write(&self.config, None, env, &self.out_dir)?; 405 | Ok(article) 406 | }) 407 | .collect::<Vec<Result<Article>>>() 408 | .into_iter() 409 | .collect::<Result<Vec<Article>>>()?; 410 | 411 | // Remove draft articles. 412 | articles.retain(|a| !a.draft); 413 | 414 | articles.sort_by_key(|a| a.date); 415 | articles.reverse(); 416 | 417 | log::info!("Build pages"); 418 | for m in pages { 419 | let page = Article::new(m); 420 | page.render_and_write(&self.config, Some(&articles), env, &self.out_dir)?; 421 | } 422 | Ok(()) 423 | } 424 | 425 | fn copy_files(&self) -> Result<()> { 426 | log::info!( 427 | "Copy files: {} => {}", 428 | self.src_dir.display(), 429 | self.out_dir.display() 430 | ); 431 | for entry in walkdir::WalkDir::new(&self.src_dir) { 432 | let entry = entry?; 433 | let src_path = entry.path(); 434 | if let Some("md") = src_path.extension().map(|ext| ext.to_str().unwrap()) { 435 | continue; 436 | } 437 | 438 | let relative_path = src_path.strip_prefix(&self.src_dir).expect(""); 439 | let out_path = self.out_dir.join(relative_path); 440 | log::debug!("{:32} => {}", relative_path.display(), out_path.display()); 441 | 442 | if src_path.is_dir() { 443 | std::fs::create_dir_all(&out_path).expect("create_dir_all failed") 444 | } else { 445 | std::fs::copy(src_path, out_path).expect("copy failed"); 446 | } 447 | } 448 | Ok(()) 449 | } 450 | } 451 | 452 | #[cfg(test)] 453 | mod tests { 454 | use super::*; 455 | 456 | #[test] 457 | fn slug_to_url_test() { 458 | assert_eq!(slug_to_url("foo"), "foo/"); 459 | assert_eq!(slug_to_url("foo/"), "foo/"); 460 | assert_eq!(slug_to_url("feed.xml"), "feed.xml"); 461 | assert_eq!(slug_to_url("feed.xml/"), "feed.xml/"); 462 | assert_eq!(slug_to_url("index"), ""); 463 | assert_eq!(slug_to_url(""), ""); 464 | assert_eq!(slug_to_url("a/b"), "a/b/"); 465 | assert_eq!(slug_to_url("a/b/"), "a/b/"); 466 | assert_eq!(slug_to_url("a/b.html"), "a/b.html"); 467 | assert_eq!(slug_to_url("a/b.html/"), "a/b.html/"); 468 | } 469 | 470 | #[test] 471 | fn url_to_filename_test() { 472 | assert_eq!(url_to_filename(""), "index.html"); 473 | assert_eq!(url_to_filename("a"), "a"); 474 | assert_eq!(url_to_filename("a/"), "a/index.html"); 475 | assert_eq!(url_to_filename("a.html"), "a.html"); 476 | assert_eq!(url_to_filename("a.html/"), "a.html/index.html"); 477 | assert_eq!(url_to_filename("a/b"), "a/b"); 478 | assert_eq!(url_to_filename("a/b/"), "a/b/index.html"); 479 | assert_eq!(url_to_filename("a/b.html"), "a/b.html"); 480 | assert_eq!(url_to_filename("a/b.html/"), "a/b.html/index.html"); 481 | } 482 | 483 | #[test] 484 | fn parse_markdowne_metadata_test() { 485 | let s = r#"title = "Hello" 486 | slug = "10th-anniversary" 487 | date = "2018-01-11" 488 | "#; 489 | assert_eq!( 490 | s.parse::<Metadata>().unwrap(), 491 | Metadata { 492 | title: "Hello".to_string(), 493 | slug: Some("10th-anniversary".to_string()), 494 | date: Some("2018-01-11".parse().unwrap()), 495 | ..Default::default() 496 | } 497 | ); 498 | } 499 | 500 | #[test] 501 | fn parse_markdown_test() { 502 | let s = r#"title = "Hello" 503 | slug = "10th-anniversary" 504 | date = "2018-01-11" 505 | 506 | hello world 507 | "#; 508 | 509 | assert_eq!( 510 | s.parse::<Markdown>().unwrap(), 511 | Markdown { 512 | metadata: Metadata { 513 | title: "Hello".to_string(), 514 | slug: Some("10th-anniversary".to_string()), 515 | date: Some("2018-01-11".parse().unwrap()), 516 | ..Default::default() 517 | }, 518 | content: "hello world\n".to_string(), 519 | } 520 | ); 521 | 522 | let s = r#"<!-- 523 | title = "Hello" 524 | --> 525 | 526 | hello world 527 | "#; 528 | assert_eq!( 529 | s.parse::<Markdown>().unwrap(), 530 | Markdown { 531 | metadata: Metadata { 532 | title: "Hello".to_string(), 533 | ..Default::default() 534 | }, 535 | content: "hello world\n".to_string(), 536 | } 537 | ); 538 | 539 | let s = r#"<!-- prettier-ignore --> 540 | title = "Hello" 541 | 542 | hello world 543 | "#; 544 | assert_eq!( 545 | s.parse::<Markdown>().unwrap(), 546 | Markdown { 547 | metadata: Metadata { 548 | title: "Hello".to_string(), 549 | ..Default::default() 550 | }, 551 | content: "hello world\n".to_string(), 552 | } 553 | ); 554 | 555 | // If the first line starts with "#", treat that as a title. 556 | let s = r#"# title 557 | 558 | <!-- prettier-ignore --> 559 | date = "2018-01-11" 560 | 561 | hello world 562 | "#; 563 | assert_eq!( 564 | s.parse::<Markdown>().unwrap(), 565 | Markdown { 566 | metadata: Metadata { 567 | title: "title".to_string(), 568 | date: Some("2018-01-11".parse().unwrap()), 569 | ..Default::default() 570 | }, 571 | content: "hello world\n".to_string(), 572 | } 573 | ); 574 | 575 | // If the first line starts with "<!--", Ignore that 576 | let s = r#"<!-- -*- apheleia-formatters: prettier -*- --> 577 | 578 | # title 579 | 580 | <!-- prettier-ignore --> 581 | date = "2018-01-11" 582 | 583 | hello world 584 | "#; 585 | assert_eq!( 586 | s.parse::<Markdown>().unwrap(), 587 | Markdown { 588 | metadata: Metadata { 589 | title: "title".to_string(), 590 | date: Some("2018-01-11".parse().unwrap()), 591 | ..Default::default() 592 | }, 593 | content: "hello world\n".to_string(), 594 | } 595 | ); 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android_system_properties" 16 | version = "0.1.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 19 | dependencies = [ 20 | "libc", 21 | ] 22 | 23 | [[package]] 24 | name = "anstream" 25 | version = "0.6.18" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 28 | dependencies = [ 29 | "anstyle", 30 | "anstyle-parse", 31 | "anstyle-query", 32 | "anstyle-wincon", 33 | "colorchoice", 34 | "is_terminal_polyfill", 35 | "utf8parse", 36 | ] 37 | 38 | [[package]] 39 | name = "anstyle" 40 | version = "1.0.10" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 43 | 44 | [[package]] 45 | name = "anstyle-parse" 46 | version = "0.2.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 49 | dependencies = [ 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-query" 55 | version = "1.1.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 58 | dependencies = [ 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "anstyle-wincon" 64 | version = "3.0.7" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 67 | dependencies = [ 68 | "anstyle", 69 | "once_cell", 70 | "windows-sys", 71 | ] 72 | 73 | [[package]] 74 | name = "anyhow" 75 | version = "1.0.100" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.4.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 84 | 85 | [[package]] 86 | name = "bitflags" 87 | version = "2.9.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 90 | 91 | [[package]] 92 | name = "bumpalo" 93 | version = "3.17.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 96 | 97 | [[package]] 98 | name = "cc" 99 | version = "1.2.17" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" 102 | dependencies = [ 103 | "shlex", 104 | ] 105 | 106 | [[package]] 107 | name = "cfg-if" 108 | version = "1.0.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 111 | 112 | [[package]] 113 | name = "chrono" 114 | version = "0.4.42" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 117 | dependencies = [ 118 | "iana-time-zone", 119 | "js-sys", 120 | "num-traits", 121 | "serde", 122 | "wasm-bindgen", 123 | "windows-link 0.2.0", 124 | ] 125 | 126 | [[package]] 127 | name = "clap" 128 | version = "4.5.35" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 131 | dependencies = [ 132 | "clap_builder", 133 | "clap_derive", 134 | ] 135 | 136 | [[package]] 137 | name = "clap_builder" 138 | version = "4.5.35" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 141 | dependencies = [ 142 | "anstream", 143 | "anstyle", 144 | "clap_lex", 145 | "strsim", 146 | ] 147 | 148 | [[package]] 149 | name = "clap_derive" 150 | version = "4.5.32" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 153 | dependencies = [ 154 | "heck", 155 | "proc-macro2", 156 | "quote", 157 | "syn", 158 | ] 159 | 160 | [[package]] 161 | name = "clap_lex" 162 | version = "0.7.4" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 165 | 166 | [[package]] 167 | name = "colorchoice" 168 | version = "1.0.3" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 171 | 172 | [[package]] 173 | name = "core-foundation-sys" 174 | version = "0.8.7" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 177 | 178 | [[package]] 179 | name = "crossbeam-deque" 180 | version = "0.8.6" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 183 | dependencies = [ 184 | "crossbeam-epoch", 185 | "crossbeam-utils", 186 | ] 187 | 188 | [[package]] 189 | name = "crossbeam-epoch" 190 | version = "0.9.18" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 193 | dependencies = [ 194 | "crossbeam-utils", 195 | ] 196 | 197 | [[package]] 198 | name = "crossbeam-utils" 199 | version = "0.8.21" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 202 | 203 | [[package]] 204 | name = "either" 205 | version = "1.15.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 208 | 209 | [[package]] 210 | name = "env_filter" 211 | version = "0.1.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 214 | dependencies = [ 215 | "log", 216 | "regex", 217 | ] 218 | 219 | [[package]] 220 | name = "env_logger" 221 | version = "0.11.8" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 224 | dependencies = [ 225 | "anstream", 226 | "anstyle", 227 | "env_filter", 228 | "jiff", 229 | "log", 230 | ] 231 | 232 | [[package]] 233 | name = "equivalent" 234 | version = "1.0.2" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 237 | 238 | [[package]] 239 | name = "getopts" 240 | version = "0.2.21" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 243 | dependencies = [ 244 | "unicode-width 0.1.14", 245 | ] 246 | 247 | [[package]] 248 | name = "glob" 249 | version = "0.3.3" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 252 | 253 | [[package]] 254 | name = "hashbrown" 255 | version = "0.15.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 258 | 259 | [[package]] 260 | name = "heck" 261 | version = "0.5.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 264 | 265 | [[package]] 266 | name = "iana-time-zone" 267 | version = "0.1.63" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 270 | dependencies = [ 271 | "android_system_properties", 272 | "core-foundation-sys", 273 | "iana-time-zone-haiku", 274 | "js-sys", 275 | "log", 276 | "wasm-bindgen", 277 | "windows-core", 278 | ] 279 | 280 | [[package]] 281 | name = "iana-time-zone-haiku" 282 | version = "0.1.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 285 | dependencies = [ 286 | "cc", 287 | ] 288 | 289 | [[package]] 290 | name = "indexmap" 291 | version = "2.11.4" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 294 | dependencies = [ 295 | "equivalent", 296 | "hashbrown", 297 | ] 298 | 299 | [[package]] 300 | name = "is_terminal_polyfill" 301 | version = "1.70.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 304 | 305 | [[package]] 306 | name = "jiff" 307 | version = "0.2.5" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" 310 | dependencies = [ 311 | "jiff-static", 312 | "log", 313 | "portable-atomic", 314 | "portable-atomic-util", 315 | "serde", 316 | ] 317 | 318 | [[package]] 319 | name = "jiff-static" 320 | version = "0.2.5" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" 323 | dependencies = [ 324 | "proc-macro2", 325 | "quote", 326 | "syn", 327 | ] 328 | 329 | [[package]] 330 | name = "js-sys" 331 | version = "0.3.77" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 334 | dependencies = [ 335 | "once_cell", 336 | "wasm-bindgen", 337 | ] 338 | 339 | [[package]] 340 | name = "libc" 341 | version = "0.2.171" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 344 | 345 | [[package]] 346 | name = "log" 347 | version = "0.4.28" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 350 | 351 | [[package]] 352 | name = "memchr" 353 | version = "2.7.4" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 356 | 357 | [[package]] 358 | name = "memo-map" 359 | version = "0.3.3" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" 362 | 363 | [[package]] 364 | name = "minijinja" 365 | version = "2.12.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "a9f264d75233323f4b7d2f03aefe8a990690cdebfbfe26ea86bcbaec5e9ac990" 368 | dependencies = [ 369 | "memo-map", 370 | "self_cell", 371 | "serde", 372 | ] 373 | 374 | [[package]] 375 | name = "num-traits" 376 | version = "0.2.19" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 379 | dependencies = [ 380 | "autocfg", 381 | ] 382 | 383 | [[package]] 384 | name = "once_cell" 385 | version = "1.21.3" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 388 | 389 | [[package]] 390 | name = "portable-atomic" 391 | version = "1.11.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 394 | 395 | [[package]] 396 | name = "portable-atomic-util" 397 | version = "0.2.4" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 400 | dependencies = [ 401 | "portable-atomic", 402 | ] 403 | 404 | [[package]] 405 | name = "proc-macro2" 406 | version = "1.0.94" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 409 | dependencies = [ 410 | "unicode-ident", 411 | ] 412 | 413 | [[package]] 414 | name = "pulldown-cmark" 415 | version = "0.13.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" 418 | dependencies = [ 419 | "bitflags", 420 | "getopts", 421 | "memchr", 422 | "pulldown-cmark-escape", 423 | "unicase", 424 | ] 425 | 426 | [[package]] 427 | name = "pulldown-cmark-escape" 428 | version = "0.11.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" 431 | 432 | [[package]] 433 | name = "quote" 434 | version = "1.0.40" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 437 | dependencies = [ 438 | "proc-macro2", 439 | ] 440 | 441 | [[package]] 442 | name = "rayon" 443 | version = "1.11.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 446 | dependencies = [ 447 | "either", 448 | "rayon-core", 449 | ] 450 | 451 | [[package]] 452 | name = "rayon-core" 453 | version = "1.13.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 456 | dependencies = [ 457 | "crossbeam-deque", 458 | "crossbeam-utils", 459 | ] 460 | 461 | [[package]] 462 | name = "regex" 463 | version = "1.11.2" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" 466 | dependencies = [ 467 | "aho-corasick", 468 | "memchr", 469 | "regex-automata", 470 | "regex-syntax", 471 | ] 472 | 473 | [[package]] 474 | name = "regex-automata" 475 | version = "0.4.9" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 478 | dependencies = [ 479 | "aho-corasick", 480 | "memchr", 481 | "regex-syntax", 482 | ] 483 | 484 | [[package]] 485 | name = "regex-syntax" 486 | version = "0.8.5" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 489 | 490 | [[package]] 491 | name = "rustversion" 492 | version = "1.0.20" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 495 | 496 | [[package]] 497 | name = "same-file" 498 | version = "1.0.6" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 501 | dependencies = [ 502 | "winapi-util", 503 | ] 504 | 505 | [[package]] 506 | name = "self_cell" 507 | version = "1.1.0" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" 510 | 511 | [[package]] 512 | name = "serde" 513 | version = "1.0.226" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" 516 | dependencies = [ 517 | "serde_core", 518 | "serde_derive", 519 | ] 520 | 521 | [[package]] 522 | name = "serde_core" 523 | version = "1.0.226" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" 526 | dependencies = [ 527 | "serde_derive", 528 | ] 529 | 530 | [[package]] 531 | name = "serde_derive" 532 | version = "1.0.226" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" 535 | dependencies = [ 536 | "proc-macro2", 537 | "quote", 538 | "syn", 539 | ] 540 | 541 | [[package]] 542 | name = "serde_spanned" 543 | version = "1.0.2" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" 546 | dependencies = [ 547 | "serde_core", 548 | ] 549 | 550 | [[package]] 551 | name = "shlex" 552 | version = "1.3.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 555 | 556 | [[package]] 557 | name = "site" 558 | version = "2.0.1" 559 | dependencies = [ 560 | "anyhow", 561 | "chrono", 562 | "clap", 563 | "env_logger", 564 | "glob", 565 | "log", 566 | "minijinja", 567 | "pulldown-cmark", 568 | "rayon", 569 | "regex", 570 | "serde", 571 | "toml", 572 | "unicode-width 0.2.1", 573 | "walkdir", 574 | ] 575 | 576 | [[package]] 577 | name = "strsim" 578 | version = "0.11.1" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 581 | 582 | [[package]] 583 | name = "syn" 584 | version = "2.0.100" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 587 | dependencies = [ 588 | "proc-macro2", 589 | "quote", 590 | "unicode-ident", 591 | ] 592 | 593 | [[package]] 594 | name = "toml" 595 | version = "0.9.7" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" 598 | dependencies = [ 599 | "indexmap", 600 | "serde_core", 601 | "serde_spanned", 602 | "toml_datetime", 603 | "toml_parser", 604 | "toml_writer", 605 | "winnow", 606 | ] 607 | 608 | [[package]] 609 | name = "toml_datetime" 610 | version = "0.7.2" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" 613 | dependencies = [ 614 | "serde_core", 615 | ] 616 | 617 | [[package]] 618 | name = "toml_parser" 619 | version = "1.0.3" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" 622 | dependencies = [ 623 | "winnow", 624 | ] 625 | 626 | [[package]] 627 | name = "toml_writer" 628 | version = "1.0.3" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" 631 | 632 | [[package]] 633 | name = "unicase" 634 | version = "2.8.1" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 637 | 638 | [[package]] 639 | name = "unicode-ident" 640 | version = "1.0.18" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 643 | 644 | [[package]] 645 | name = "unicode-width" 646 | version = "0.1.14" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 649 | 650 | [[package]] 651 | name = "unicode-width" 652 | version = "0.2.1" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 655 | 656 | [[package]] 657 | name = "utf8parse" 658 | version = "0.2.2" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 661 | 662 | [[package]] 663 | name = "walkdir" 664 | version = "2.5.0" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 667 | dependencies = [ 668 | "same-file", 669 | "winapi-util", 670 | ] 671 | 672 | [[package]] 673 | name = "wasm-bindgen" 674 | version = "0.2.100" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 677 | dependencies = [ 678 | "cfg-if", 679 | "once_cell", 680 | "rustversion", 681 | "wasm-bindgen-macro", 682 | ] 683 | 684 | [[package]] 685 | name = "wasm-bindgen-backend" 686 | version = "0.2.100" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 689 | dependencies = [ 690 | "bumpalo", 691 | "log", 692 | "proc-macro2", 693 | "quote", 694 | "syn", 695 | "wasm-bindgen-shared", 696 | ] 697 | 698 | [[package]] 699 | name = "wasm-bindgen-macro" 700 | version = "0.2.100" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 703 | dependencies = [ 704 | "quote", 705 | "wasm-bindgen-macro-support", 706 | ] 707 | 708 | [[package]] 709 | name = "wasm-bindgen-macro-support" 710 | version = "0.2.100" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 713 | dependencies = [ 714 | "proc-macro2", 715 | "quote", 716 | "syn", 717 | "wasm-bindgen-backend", 718 | "wasm-bindgen-shared", 719 | ] 720 | 721 | [[package]] 722 | name = "wasm-bindgen-shared" 723 | version = "0.2.100" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 726 | dependencies = [ 727 | "unicode-ident", 728 | ] 729 | 730 | [[package]] 731 | name = "winapi-util" 732 | version = "0.1.9" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 735 | dependencies = [ 736 | "windows-sys", 737 | ] 738 | 739 | [[package]] 740 | name = "windows-core" 741 | version = "0.61.0" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 744 | dependencies = [ 745 | "windows-implement", 746 | "windows-interface", 747 | "windows-link 0.1.1", 748 | "windows-result", 749 | "windows-strings", 750 | ] 751 | 752 | [[package]] 753 | name = "windows-implement" 754 | version = "0.60.0" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 757 | dependencies = [ 758 | "proc-macro2", 759 | "quote", 760 | "syn", 761 | ] 762 | 763 | [[package]] 764 | name = "windows-interface" 765 | version = "0.59.1" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 768 | dependencies = [ 769 | "proc-macro2", 770 | "quote", 771 | "syn", 772 | ] 773 | 774 | [[package]] 775 | name = "windows-link" 776 | version = "0.1.1" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 779 | 780 | [[package]] 781 | name = "windows-link" 782 | version = "0.2.0" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 785 | 786 | [[package]] 787 | name = "windows-result" 788 | version = "0.3.2" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 791 | dependencies = [ 792 | "windows-link 0.1.1", 793 | ] 794 | 795 | [[package]] 796 | name = "windows-strings" 797 | version = "0.4.0" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 800 | dependencies = [ 801 | "windows-link 0.1.1", 802 | ] 803 | 804 | [[package]] 805 | name = "windows-sys" 806 | version = "0.59.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 809 | dependencies = [ 810 | "windows-targets", 811 | ] 812 | 813 | [[package]] 814 | name = "windows-targets" 815 | version = "0.52.6" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 818 | dependencies = [ 819 | "windows_aarch64_gnullvm", 820 | "windows_aarch64_msvc", 821 | "windows_i686_gnu", 822 | "windows_i686_gnullvm", 823 | "windows_i686_msvc", 824 | "windows_x86_64_gnu", 825 | "windows_x86_64_gnullvm", 826 | "windows_x86_64_msvc", 827 | ] 828 | 829 | [[package]] 830 | name = "windows_aarch64_gnullvm" 831 | version = "0.52.6" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 834 | 835 | [[package]] 836 | name = "windows_aarch64_msvc" 837 | version = "0.52.6" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 840 | 841 | [[package]] 842 | name = "windows_i686_gnu" 843 | version = "0.52.6" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 846 | 847 | [[package]] 848 | name = "windows_i686_gnullvm" 849 | version = "0.52.6" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 852 | 853 | [[package]] 854 | name = "windows_i686_msvc" 855 | version = "0.52.6" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 858 | 859 | [[package]] 860 | name = "windows_x86_64_gnu" 861 | version = "0.52.6" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 864 | 865 | [[package]] 866 | name = "windows_x86_64_gnullvm" 867 | version = "0.52.6" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 870 | 871 | [[package]] 872 | name = "windows_x86_64_msvc" 873 | version = "0.52.6" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 876 | 877 | [[package]] 878 | name = "winnow" 879 | version = "0.7.13" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 882 | --------------------------------------------------------------------------------