├── .gitignore ├── docs ├── .gitignore ├── static │ ├── fonts │ │ ├── IBMPlexSerif-Bold.otf │ │ ├── SourceSerif4-Bold.otf │ │ ├── SourceSerif4-It.otf │ │ ├── SourceSerif4-BoldIt.otf │ │ └── SourceSerif4-Regular.otf │ └── style.css ├── mksite.toml ├── layout │ └── _.html └── src │ └── index.md ├── mksite.default.toml ├── Makefile ├── src ├── cli │ ├── build.rs │ ├── init.rs │ ├── clean.rs │ └── new.rs ├── cli.rs ├── util.rs ├── main.rs ├── error.rs ├── transform.rs ├── config.rs └── site.rs ├── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── mksite.yml ├── mksite.1.scd ├── mksite.1 ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | -------------------------------------------------------------------------------- /docs/static/fonts/IBMPlexSerif-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alterae/mksite/HEAD/docs/static/fonts/IBMPlexSerif-Bold.otf -------------------------------------------------------------------------------- /docs/static/fonts/SourceSerif4-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alterae/mksite/HEAD/docs/static/fonts/SourceSerif4-Bold.otf -------------------------------------------------------------------------------- /docs/static/fonts/SourceSerif4-It.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alterae/mksite/HEAD/docs/static/fonts/SourceSerif4-It.otf -------------------------------------------------------------------------------- /docs/static/fonts/SourceSerif4-BoldIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alterae/mksite/HEAD/docs/static/fonts/SourceSerif4-BoldIt.otf -------------------------------------------------------------------------------- /docs/static/fonts/SourceSerif4-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alterae/mksite/HEAD/docs/static/fonts/SourceSerif4-Regular.otf -------------------------------------------------------------------------------- /docs/mksite.toml: -------------------------------------------------------------------------------- 1 | [data] 2 | base_path = "/mksite" 3 | github = "https://github.com/alterae/mksite" 4 | crate = "https://crates.io/crates/mksite" 5 | 6 | [transforms] 7 | md.html = "pandoc -f markdown -t html5 --number-offset=1" 8 | -------------------------------------------------------------------------------- /mksite.default.toml: -------------------------------------------------------------------------------- 1 | # The contents of the data section are made available in template rendering. 2 | [data] 3 | # foo = "bar" 4 | 5 | # Transforms are used to further transform content after template rendering. 6 | [transforms] 7 | # md.html = "pandoc -f markdown -t html" 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build clean clean-docs docs serve 2 | 3 | all: docs 4 | 5 | build: 6 | @cargo build 7 | 8 | clean: 9 | @cargo clean 10 | 11 | clean-docs: 12 | @cd docs && cargo run clean 13 | 14 | docs: 15 | @cd docs && cargo run build 16 | 17 | serve: clean-docs docs 18 | @pnpx serve docs/out 19 | -------------------------------------------------------------------------------- /src/cli/build.rs: -------------------------------------------------------------------------------- 1 | //! The `mksite build` subcommand. 2 | 3 | use crate::{config, site}; 4 | 5 | use crate::Result; 6 | 7 | /// Loads all the templates in the `src/` directory and renders them using the 8 | /// metadata defined in `mksite.toml`. 9 | pub(crate) fn cmd() -> Result<()> { 10 | site::Site::new(config::load()?)?.build() 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/init.rs: -------------------------------------------------------------------------------- 1 | //! The `mksite init` subcommand. 2 | 3 | use std::path::Path; 4 | 5 | use crate::config; 6 | 7 | /// Generates a skeleton `mksite.toml` config file in the current directory. 8 | pub(crate) fn cmd() -> crate::Result<()> { 9 | log::info!("Writing default config file to './{}'", config::FILE_NAME); 10 | config::generate(Path::new(".")) 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/clean.rs: -------------------------------------------------------------------------------- 1 | //! The `mksite clean` subcommand. 2 | 3 | use std::{fs, io}; 4 | 5 | use crate::{config, Result}; 6 | 7 | /// Deletes the `out/` directory and all its contents. 8 | pub(crate) fn cmd() -> Result<()> { 9 | let config = config::load()?; 10 | 11 | log::info!("Removing '{}/'", config.dirs.out.display()); 12 | 13 | fs::remove_dir_all(&config.dirs.out).or_else(|e| match e.kind() { 14 | io::ErrorKind::NotFound => { 15 | log::warn!("Cannot remove '{}': {e}", config.dirs.out.display()); 16 | Ok(()) 17 | } 18 | _ => Err(crate::Error::Io { 19 | msg: format!("Cannot remove '{}'", config.dirs.out.display()), 20 | source: e, 21 | }), 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/cli/new.rs: -------------------------------------------------------------------------------- 1 | //! The `mksite new` subcommand. 2 | 3 | use std::{fs, path::Path}; 4 | 5 | use crate::{config, Error}; 6 | 7 | /// Creates a new directory with a given name, and then initializes a basic 8 | /// project structure within, containing a `mksite.toml` file and a `src/`, 9 | /// `static/`, and `layout/` directories. 10 | pub(crate) fn cmd(name: String) -> crate::Result<()> { 11 | log::info!("Creating new project scaffold in '{name}/'"); 12 | 13 | for dir in ["src", "static", "layout"] { 14 | let path = Path::new(&name).join(dir); 15 | fs::create_dir_all(path).map_err(|source| Error::Io { 16 | msg: format!("Cannot create '{name}/{dir}/'"), 17 | source, 18 | })?; 19 | } 20 | 21 | config::generate(Path::new(&name)) 22 | } 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mksite" 3 | version = "0.1.2" 4 | authors = ["alterae "] 5 | edition = "2021" 6 | description = "A file format-agnostic static site generator" 7 | homepage = "https://alterae.github.io/mksite" 8 | repository = "https://github.com/alterae/mksite" 9 | license = "MIT" 10 | keywords = ["ssg", "static", "site", "generator"] 11 | categories = ["command-line-utilities"] 12 | exclude = ["/docs", "/.github", "/Makefile"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | clap = { version = "4.5.18", features = ["derive"] } 18 | colored = "2.1.0" 19 | fern = "0.6.2" 20 | log = "0.4.22" 21 | maplit = "1.0.2" 22 | serde = { version = "1.0.210", features = ["derive"] } 23 | shell-words = "1.1.0" 24 | tera = "1.20.0" 25 | thiserror = "1.0.63" 26 | toml = "0.8.19" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 alterae 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/layout/_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | mksite - a file format-agnostic static site generator 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |

mksite

22 |
23 | 34 |
35 |
{{ page.content | safe }}
36 | 37 | 38 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Command-line interface definition and argument handling. 2 | 3 | pub(crate) mod build; 4 | mod clean; 5 | mod init; 6 | mod new; 7 | 8 | /// A file format-agnostic static site generator. 9 | #[derive(clap::Parser)] 10 | #[command(version)] 11 | pub(crate) struct Args { 12 | /// The subcommand to run. 13 | #[command(subcommand)] 14 | pub(crate) command: Command, 15 | 16 | /// Do not print log messages. 17 | #[arg(short, long, conflicts_with = "log_level")] 18 | pub(crate) quiet: bool, 19 | 20 | /// What level of logging to enable (error, warn, info, debug, or trace). 21 | #[arg(long, default_value = "info")] 22 | pub(crate) log_level: log::LevelFilter, 23 | } 24 | 25 | /// Enum of subcommands. 26 | #[derive(clap::Subcommand)] 27 | pub(crate) enum Command { 28 | /// Build the site according to `mksite.toml`. 29 | Build, 30 | 31 | /// Delete all build outputs. 32 | Clean, 33 | 34 | /// Initialize a `mksite.toml` file in the current directory. 35 | Init, 36 | 37 | /// Scaffold an empty site in a new directory. 38 | New { 39 | /// The name of the directory to create. 40 | name: String, 41 | }, 42 | } 43 | 44 | impl Command { 45 | /// Runs the given command. 46 | pub(crate) fn run(self) -> crate::Result<()> { 47 | match self { 48 | Self::Build => build::cmd(), 49 | Self::Clean => clean::cmd(), 50 | Self::Init => init::cmd(), 51 | Self::New { name } => new::cmd(name), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Miscellaneous utility fns that don't go anywhere else. 2 | 3 | use crate::Error; 4 | 5 | use std::fs; 6 | 7 | use std::path::PathBuf; 8 | 9 | use crate::Result; 10 | 11 | use std::path::Path; 12 | 13 | /// Walks a directory and returns every path in it. Probably not very performant. 14 | pub(crate) fn walk_dir(dir: impl AsRef) -> Result> { 15 | let read_dir = fs::read_dir(&dir).map_err(|source| Error::Io { 16 | msg: format!("Cannot read source directory '{}'", dir.as_ref().display()), 17 | source, 18 | })?; 19 | 20 | let mut res = Vec::new(); 21 | 22 | for entry in read_dir { 23 | let entry = entry.map_err(|source| Error::Io { 24 | msg: format!("Cannot get entry in '{}'", dir.as_ref().display()), 25 | source, 26 | })?; 27 | 28 | let path = entry.path(); 29 | 30 | if path.is_dir() { 31 | res.append(&mut walk_dir(path)?); 32 | } else { 33 | res.push(path); 34 | } 35 | } 36 | 37 | Ok(res) 38 | } 39 | 40 | /// Strips the `old` prefix from a path and replaces it with `new`. 41 | pub(crate) fn swap_prefix( 42 | path: impl AsRef, 43 | old: impl AsRef, 44 | new: impl AsRef, 45 | ) -> Result { 46 | let stripped = path 47 | .as_ref() 48 | .strip_prefix(&old) 49 | .map_err(|source| Error::StripPath { 50 | path: path.as_ref().to_owned(), 51 | prefix: old.as_ref().to_owned(), 52 | source, 53 | })?; 54 | Ok(new.as_ref().join(stripped)) 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/mksite.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a mksite site to GitHub Pages 2 | name: Deploy mksite site to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: "main" 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | # Default to bash 24 | defaults: 25 | run: 26 | shell: bash 27 | 28 | jobs: 29 | # Build job 30 | build: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Install pandoc 34 | run: | 35 | sudo apt install pandoc 36 | - name: Checkout 37 | uses: actions/checkout@v3 38 | with: 39 | submodules: recursive 40 | - name: Setup Pages 41 | id: pages 42 | uses: actions/configure-pages@v2 43 | - name: Build with mksite 44 | run: | 45 | make 46 | - name: Upload artifact 47 | uses: actions/upload-pages-artifact@v1 48 | with: 49 | path: ./docs/out 50 | 51 | # Deployment job 52 | deploy: 53 | environment: 54 | name: github-pages 55 | url: ${{ steps.deployment.outputs.page_url }} 56 | runs-on: ubuntu-latest 57 | needs: build 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v1 62 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Binary crate root for `mksite`. 2 | #![warn(clippy::missing_docs_in_private_items)] 3 | 4 | use std::process::exit; 5 | 6 | use clap::Parser; 7 | 8 | mod cli; 9 | mod config; 10 | mod error; 11 | mod site; 12 | mod transform; 13 | mod util; 14 | 15 | pub(crate) use error::*; 16 | 17 | fn main() { 18 | let args = cli::Args::parse(); 19 | 20 | setup_logger(args.log_level, args.quiet).unwrap_or_else(|e| log::error!("{e}")); 21 | 22 | args.command.run().unwrap_or_else(|e| { 23 | log::error!("{e}"); 24 | exit(1) 25 | }); 26 | } 27 | 28 | /// Initializes the global logger via `fern` to the specified verbosity level. 29 | /// If `quiet` is true, no output will be printed. 30 | fn setup_logger( 31 | level: log::LevelFilter, 32 | quiet: bool, 33 | ) -> std::result::Result<(), log::SetLoggerError> { 34 | use colored::{Color, Colorize}; 35 | 36 | fern::Dispatch::new() 37 | .format(move |out, message, record| { 38 | let color = match record.level() { 39 | log::Level::Error => Color::BrightRed, 40 | log::Level::Warn => Color::BrightYellow, 41 | log::Level::Trace => Color::BrightBlack, 42 | _ => Color::White, 43 | }; 44 | 45 | out.finish(format_args!( 46 | "{}{message}", 47 | match record.level() { 48 | log::Level::Info => "".to_owned(), 49 | level => format!( 50 | "{}: ", 51 | level.as_str().to_ascii_lowercase().bold().color(color) 52 | ), 53 | }, 54 | )) 55 | }) 56 | .level(if quiet { log::LevelFilter::Off } else { level }) 57 | .chain(std::io::stderr()) 58 | .apply() 59 | } 60 | -------------------------------------------------------------------------------- /mksite.1.scd: -------------------------------------------------------------------------------- 1 | mksite(1) 2 | 3 | # NAME 4 | 5 | mksite - generate a website from template files 6 | 7 | # SYNOPSIS 8 | 9 | *mksite* [*-q* | *--log-level* _level_ ] _command_++ 10 | *mksite -V*++ 11 | *mksite -h* 12 | 13 | # DESCRIPTION 14 | 15 | *mksite* is a tool for generating a static website using the information in the 16 | \‘mksite.toml’ file in the working directory, the template files in the 17 | configured source directory, and the static assets in the configured asset 18 | directory, if any. 19 | 20 | # OPTIONS 21 | 22 | *-q*, *--quiet* 23 | Do not print log messages. Conflicts with *--log-level* option. 24 | 25 | *--log-level* _LEVEL_ 26 | Enable logging at the given level (error, warn, info, or debug). Defaults to 27 | info. Conflicts with *-q*. 28 | 29 | *-h*, *--help* 30 | Print help information and exit. 31 | 32 | *-V*, *--version* 33 | Print version information and exit. 34 | 35 | ## COMMANDS 36 | 37 | *build* 38 | Build the website according to the \‘mksite.toml’ config file. 39 | 40 | *clean* 41 | Delete all build outputs. 42 | 43 | *init* 44 | Initialize a scaffold \‘mksite.toml’ file in the current working directory. 45 | 46 | *new* _NAME_ 47 | Create a new directory _NAME_ and scaffold a website and config file in it. 48 | 49 | *help* [_SUBCOMMAND_] 50 | Print help information or the help of the given subcommand. 51 | 52 | # EXIT STATUS 53 | 54 | *mksite* exits 0 on success, 2 if the provided command-line arguments are 55 | incorrect, and 1 if any other error occurs. 56 | 57 | # FILES 58 | 59 | In order to run the *build* or *clean* subcommands, *mksite* requires the 60 | precense of a file named ‘mksite.toml’ in the working directory. This file is 61 | generated by the *init* and *new* subcommands. 62 | 63 | # NOTES 64 | 65 | For documentation on the configuration format and expected directory structure, 66 | see https://alterae.github.io/mksite. 67 | 68 | For documentation on the Tera templating language, which *mksite* uses, see 69 | https://tera.netlify.app. 70 | 71 | # BUGS 72 | 73 | *mksite* is in active development, and highly incomplete. Many important 74 | features are missing. 75 | 76 | # AUTHORS 77 | 78 | *mksite* is maintained by alterae . 79 | 80 | |[ *Website:* 81 | :[ https://alterae.github.io/mksite 82 | | *Source code:* 83 | : https://github.com/alterae/mksite 84 | 85 | # REPORTING BUGS 86 | 87 | Issues can be filed at https://github.com/alterae/mksite/issues. 88 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Custom error type. 2 | use std::{io, path}; 3 | 4 | /// Custom error type for all the errors that our code can generate. 5 | #[derive(Debug, thiserror::Error)] 6 | pub(crate) enum Error { 7 | /// An I/O error occurred. 8 | #[error("{msg}: {source}")] 9 | Io { 10 | /// A message detailing what context the error occurred in. 11 | msg: String, 12 | 13 | /// The wrapped error that caused this error. 14 | source: io::Error, 15 | }, 16 | 17 | // FIXME: improve error message 18 | /// Tera templating failed. Wraps [tera::Error]. 19 | #[error(transparent)] 20 | Tera(#[from] tera::Error), 21 | 22 | /// Deserializing TOML failed. Wraps [toml::de::Error]. 23 | #[error("Cannot deserialize {}: {0}", crate::config::FILE_NAME)] 24 | Toml(#[from] toml::de::Error), 25 | 26 | /// Initializing the logger failed because [log::set_logger] has already 27 | /// been called. Wraps [log::SetLoggerError]. 28 | #[error(transparent)] 29 | Log(#[from] log::SetLoggerError), 30 | 31 | /// Parsing a shell command failed. 32 | #[error("Cannot parse `{command}': {source}")] 33 | Shell { 34 | /// The command that failed to parse. 35 | command: String, 36 | 37 | /// The wrapped error that caused this error. 38 | source: shell_words::ParseError, 39 | }, 40 | 41 | /// Attempting to strip the prefix from a file path failed. 42 | #[error("Cannot strip prefix '{prefix}' from '{path}': {source}")] 43 | StripPath { 44 | /// The path that could not be stripped. 45 | path: path::PathBuf, 46 | 47 | /// The prefix that could not be stripped from the path. 48 | prefix: path::PathBuf, 49 | 50 | /// The wrapped error that caused this error. 51 | source: path::StripPrefixError, 52 | }, 53 | 54 | /// Converting a string from a stream of bytes failed. 55 | #[error("{msg}: {source}")] 56 | FromUtf8 { 57 | /// A message detailing what context the error occured in. 58 | msg: String, 59 | 60 | /// The wrapped error that caused this error. 61 | source: std::string::FromUtf8Error, 62 | }, 63 | 64 | // debug instead of display bc escaping the things is actually useful here 65 | /// A path was invalid UTF-8. This is distinct from [Error::FromUtf8] in 66 | /// that it occurs when converting a _path_ to UTF-8, while [Error::FromUtf8] 67 | /// is for errors converting byte vecs. 68 | #[error("Invalid UTF-8 in path {0:?}")] 69 | PathConversion(path::PathBuf), 70 | } 71 | 72 | /// Custom result wrapper that represents either success or failure. 73 | pub(crate) type Result = std::result::Result; 74 | -------------------------------------------------------------------------------- /src/transform.rs: -------------------------------------------------------------------------------- 1 | //! Transforms and their application. 2 | 3 | use std::{ 4 | io::Write, 5 | process::{Command, Stdio}, 6 | }; 7 | 8 | use crate::{Error, Result}; 9 | 10 | /// A transform is a command or pipeline of command for transforming content. 11 | /// Transforms take an input string on standard input and return an output 12 | /// string on standard output. 13 | #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] 14 | #[serde(untagged)] 15 | pub(crate) enum Transform { 16 | /// A transform with only one command. 17 | /// 18 | /// ## Example 19 | /// ```toml 20 | /// [transforms] 21 | /// md.html = "pandoc -f markdown -t html" 22 | /// ``` 23 | Single(String), 24 | 25 | /// A transforms with multiple commands. The output of each command is piped 26 | /// as the input to the next. 27 | /// 28 | /// ## Example 29 | /// ```toml 30 | /// [transforms] 31 | /// scd.html = [ "scdoc", "pandoc -f " ] 32 | Chain(Vec), 33 | } 34 | 35 | impl Transform { 36 | /// Tries to apply this transform to the given input, and returns the 37 | /// output. 38 | pub(crate) fn apply(&self, input: &[u8]) -> Result> { 39 | match self { 40 | Self::Single(command) => exec(input.into(), command), 41 | Self::Chain(commands) => commands.iter().try_fold(input.into(), exec), 42 | } 43 | } 44 | } 45 | 46 | impl std::fmt::Display for Transform { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | write!( 49 | f, 50 | "`{}'", 51 | match self { 52 | Transform::Single(cmd) => cmd.clone(), 53 | Transform::Chain(cmds) => cmds.join(" | "), 54 | } 55 | ) 56 | } 57 | } 58 | 59 | /// Tries to run a shell command with the given input, and returns the output. 60 | pub(crate) fn exec(input: Vec, command: &String) -> Result> { 61 | let argv = shell_words::split(command).map_err(|source| Error::Shell { 62 | command: command.clone(), 63 | source, 64 | })?; 65 | 66 | log::debug!("Running {argv:?}"); 67 | 68 | let mut proc = Command::new(&argv[0]) 69 | .args(&argv[1..]) 70 | .stdin(Stdio::piped()) 71 | .stdout(Stdio::piped()) 72 | .spawn() 73 | .map_err(|source| Error::Io { 74 | msg: format!("Cannot run `{command}'"), 75 | source, 76 | })?; 77 | 78 | proc.stdin 79 | .take() 80 | .expect("Child process stdin was None, which should be impossible") 81 | .write_all(&input) 82 | .map_err(|source| Error::Io { 83 | msg: format!("Cannot pipe to `{command}'"), 84 | source, 85 | })?; 86 | 87 | let output = proc.wait_with_output().map_err(|source| Error::Io { 88 | msg: format!("Error while waiting on `{command}'"), 89 | source, 90 | })?; 91 | 92 | Ok(output.stdout) 93 | } 94 | -------------------------------------------------------------------------------- /docs/static/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Source Serif 4"; 3 | src: url("/fonts/SourceSerif4-Regular.otf") format("opentype"); 4 | } 5 | 6 | @font-face { 7 | font-family: "Source Serif 4"; 8 | src: url("/fonts/SourceSerif4-Bold.otf") format("opentype"); 9 | font-weight: bold; 10 | } 11 | 12 | @font-face { 13 | font-family: "Source Serif 4"; 14 | src: url("/fonts/SourceSerif4-It.otf") format("opentype"); 15 | font-style: italic; 16 | } 17 | 18 | @font-face { 19 | font-family: "Source Serif 4"; 20 | src: url("/fonts/SourceSerif4-BoldIt.otf") format("opentype"); 21 | font-weight: bold; 22 | font-style: italic; 23 | } 24 | 25 | @font-face { 26 | font-family: "IBM Plex Serif"; 27 | src: url("/fonts/IBMPlexSerif-Bold.otf") format("opentype"); 28 | font-weight: bold; 29 | } 30 | 31 | :root { 32 | --bg: #fff; 33 | --fg: #111; 34 | 35 | font-family: "Source Serif 4", serif; 36 | line-height: 1.5; 37 | text-justify: inter-word; 38 | 39 | background-color: var(--bg); 40 | color: var(--fg); 41 | } 42 | 43 | @media screen and (prefers-color-scheme: dark) { 44 | :root { 45 | --bg: #111; 46 | --fg: #eee; 47 | } 48 | } 49 | 50 | * { 51 | box-sizing: border-box; 52 | margin: 0; 53 | padding: 0; 54 | } 55 | 56 | main { 57 | max-width: 80ch; 58 | margin: auto; 59 | } 60 | 61 | @media (max-width: calc(80ch + 3rem)) { 62 | main { 63 | margin-left: 1.5rem; 64 | margin-right: 1.5rem; 65 | } 66 | } 67 | 68 | body > header { 69 | display: flex; 70 | padding: 1.5rem; 71 | justify-content: space-between; 72 | } 73 | 74 | nav ul { 75 | list-style: none; 76 | display: flex; 77 | gap: 1rem; 78 | } 79 | 80 | header > a { 81 | text-decoration: none; 82 | } 83 | 84 | body > header h1 { 85 | font-size: 1rem; 86 | margin: 0; 87 | } 88 | 89 | h1 { 90 | font-size: 1.5rem; 91 | margin-bottom: 1rem; 92 | } 93 | 94 | h2 { 95 | font-size: 1.25rem; 96 | margin-top: 0.7rem; 97 | margin-bottom: 0.3rem; 98 | } 99 | 100 | h1, 101 | h2, 102 | h3 { 103 | font-family: "IBM Plex Serif", serif; 104 | font-weight: bold; 105 | } 106 | 107 | pre, 108 | code { 109 | font-family: "input-mono-narrow", monospace; 110 | font-size: 0.8rem; 111 | overflow: auto; 112 | } 113 | 114 | pre { 115 | padding-bottom: 0.75rem; 116 | } 117 | 118 | h1 > code { 119 | font-size: 1em; 120 | } 121 | 122 | p, 123 | aside { 124 | margin-bottom: 0.5rem; 125 | text-align: justify; 126 | hyphens: auto; 127 | } 128 | 129 | a { 130 | color: var(--fg); 131 | } 132 | 133 | abbr { 134 | letter-spacing: 0.04rem; 135 | text-decoration: none; 136 | } 137 | 138 | address { 139 | font-style: unset; 140 | } 141 | 142 | dt { 143 | font-style: italic; 144 | } 145 | 146 | dt::after { 147 | content: ":"; 148 | } 149 | 150 | dd { 151 | margin-left: 2rem; 152 | } 153 | 154 | aside, 155 | blockquote { 156 | border-left: 0.1rem solid var(--fg); 157 | padding-left: 2rem; 158 | } 159 | -------------------------------------------------------------------------------- /mksite.1: -------------------------------------------------------------------------------- 1 | .\" Generated by scdoc 1.11.1 2 | .\" Complete documentation for this program is not available as a GNU info page 3 | .ie \n(.g .ds Aq \(aq 4 | .el .ds Aq ' 5 | .nh 6 | .ad l 7 | .\" Begin generated content: 8 | .TH "mksite" "1" "2022-10-18" 9 | .P 10 | .SH NAME 11 | .P 12 | mksite - generate a website from template files 13 | .P 14 | .SH SYNOPSIS 15 | .P 16 | \fBmksite\fR [\fB-q\fR | \fB--log-level\fR \fIlevel\fR ] \fIcommand\fR 17 | .br 18 | \fBmksite -V\fR 19 | .br 20 | \fBmksite -h\fR 21 | .P 22 | .SH DESCRIPTION 23 | .P 24 | \fBmksite\fR is a tool for generating a static website using the information in the 25 | ‘mksite.\&toml’ file in the working directory, the template files in the 26 | configured source directory, and the static assets in the configured asset 27 | directory, if any.\& 28 | .P 29 | .SH OPTIONS 30 | .P 31 | \fB-q\fR, \fB--quiet\fR 32 | .RS 4 33 | Do not print log messages.\& Conflicts with \fB--log-level\fR option.\& 34 | .P 35 | .RE 36 | \fB--log-level\fR \fILEVEL\fR 37 | .RS 4 38 | Enable logging at the given level (error, warn, info, or debug).\& Defaults to 39 | info.\& Conflicts with \fB-q\fR.\& 40 | .P 41 | .RE 42 | \fB-h\fR, \fB--help\fR 43 | .RS 4 44 | Print help information and exit.\& 45 | .P 46 | .RE 47 | \fB-V\fR, \fB--version\fR 48 | .RS 4 49 | Print version information and exit.\& 50 | .P 51 | .RE 52 | .SS COMMANDS 53 | .P 54 | \fBbuild\fR 55 | .RS 4 56 | Build the website according to the ‘mksite.\&toml’ config file.\& 57 | .P 58 | .RE 59 | \fBclean\fR 60 | .RS 4 61 | Delete all build outputs.\& 62 | .P 63 | .RE 64 | \fBinit\fR 65 | .RS 4 66 | Initialize a scaffold ‘mksite.\&toml’ file in the current working directory.\& 67 | .P 68 | .RE 69 | \fBnew\fR \fINAME\fR 70 | .RS 4 71 | Create a new directory \fINAME\fR and scaffold a website and config file in it.\& 72 | .P 73 | .RE 74 | \fBhelp\fR [\fISUBCOMMAND\fR] 75 | .RS 4 76 | Print help information or the help of the given subcommand.\& 77 | .P 78 | .RE 79 | .SH EXIT STATUS 80 | .P 81 | \fBmksite\fR exits 0 on success, 2 if the provided command-line arguments are 82 | incorrect, and 1 if any other error occurs.\& 83 | .P 84 | .SH FILES 85 | .P 86 | In order to run the \fBbuild\fR or \fBclean\fR subcommands, \fBmksite\fR requires the 87 | precense of a file named ‘mksite.\&toml’ in the working directory.\& This file is 88 | generated by the \fBinit\fR and \fBnew\fR subcommands.\& 89 | .P 90 | .SH NOTES 91 | .P 92 | For documentation on the configuration format and expected directory structure, 93 | see https://alterae.\&github.\&io/mksite.\& 94 | .P 95 | For documentation on the Tera templating language, which \fBmksite\fR uses, see 96 | https://tera.\&netlify.\&app.\& 97 | .P 98 | .SH BUGS 99 | .P 100 | \fBmksite\fR is in active development, and highly incomplete.\& Many important 101 | features are missing.\& 102 | .P 103 | .SH AUTHORS 104 | .P 105 | \fBmksite\fR is maintained by alterae .\& 106 | .P 107 | .TS 108 | l l 109 | l l. 110 | T{ 111 | \fBWebsite:\fR 112 | T} T{ 113 | https://alterae.\&github.\&io/mksite 114 | T} 115 | T{ 116 | \fBSource code:\fR 117 | T} T{ 118 | https://github.\&com/alterae/mksite 119 | T} 120 | .TE 121 | .sp 1 122 | .SH REPORTING BUGS 123 | .P 124 | Issues can be filed at https://github.\&com/alterae/mksite/issues.\& 125 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Config file generation, parsing, and loading. 2 | 3 | use std::{ 4 | collections::HashMap, 5 | fs, 6 | io::Write, 7 | path::{self, PathBuf}, 8 | }; 9 | 10 | use crate::{Error, Result}; 11 | 12 | use crate::transform; 13 | 14 | /// The name of the config file to use. 15 | pub(crate) const FILE_NAME: &str = "mksite.toml"; 16 | 17 | /// The configuration for a `mksite` project. 18 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 19 | #[serde(default)] 20 | pub(crate) struct Config { 21 | /// The list of important directories. 22 | #[serde(default)] 23 | pub(crate) dirs: Dirs, 24 | 25 | /// The list of pages to ignore in the templating, transforming, and layout 26 | /// steps. 27 | #[serde(default)] 28 | pub(crate) ignores: Ignores, 29 | 30 | /// Data to be passed to template rendering. 31 | #[serde(default)] 32 | pub(crate) data: HashMap, 33 | 34 | /// The list of transforms to apply, stored as a map of input formats to 35 | /// sub-maps of output formats and transforms. 36 | #[serde(default)] 37 | pub(crate) transforms: HashMap>, 38 | } 39 | 40 | /// The names of all the important directories needed to build a site. 41 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 42 | pub(crate) struct Dirs { 43 | /// The src directory holds template files to be rendered, transformed, and 44 | /// inserted into layouts. 45 | /// 46 | /// The `serde` default is provided by the function [`Dirs::default_src`](Dirs::default_src). 47 | #[serde(default = "Dirs::default_src")] 48 | pub(crate) src: PathBuf, 49 | 50 | /// The out directory is where generated content goes. 51 | /// 52 | /// The `serde` default is provided by the function [`Dirs::default_src`](Dirs::default_out). 53 | #[serde(default = "Dirs::default_out")] 54 | pub(crate) out: PathBuf, 55 | 56 | /// Files in the static directory are copied as-is to the out directory. 57 | /// 58 | /// The `serde` default is provided by the function [`Dirs::default_src`](Dirs::default_static). 59 | #[serde(default = "Dirs::default_static")] 60 | pub(crate) r#static: PathBuf, 61 | 62 | /// The layout directory is where layout files are stored. 63 | /// 64 | /// The `serde` default is provided by the function [`Dirs::default_src`](Dirs::default_layout). 65 | #[serde(default = "Dirs::default_layout")] 66 | pub(crate) layout: PathBuf, 67 | } 68 | 69 | // TODO: use git-style globs for ignore paths 70 | // TODO: document in readme 71 | /// The paths to files to be ignored during the templating, transform, and 72 | /// layout steps. 73 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 74 | pub(crate) struct Ignores { 75 | /// Paths to source pages (eg `src/index.html`) to be ignored during 76 | /// templating. Pages ignored this way will not be passed through Tera, and 77 | /// as such do not have to be valid UTF-8. 78 | #[serde(default)] 79 | pub(crate) template: Vec, 80 | 81 | /// Paths to source pages (eg `src/index.html`) to be ignored during the 82 | /// transform step. Pages ignored this way will not be transformed, and 83 | /// their file extension will remain preserved, as if no transform were 84 | /// defined for them. 85 | #[serde(default)] 86 | pub(crate) transform: Vec, 87 | 88 | /// Paths to _output_ pages (eg `out/index.html`) to be ignored during the 89 | /// layout step. Pages ignored this way will not have layouts applied to 90 | /// them, and will be written to the output directory as-is, as if no layout 91 | /// were defined from them. As a result, they do not have to be valid UTF-8. 92 | #[serde(default)] 93 | pub(crate) layout: Vec, 94 | } 95 | 96 | impl Dirs { 97 | /// Returns the default 'src/' directory. 98 | fn default_src() -> PathBuf { 99 | "src".into() 100 | } 101 | 102 | /// Returns the default 'out/' directory. 103 | fn default_out() -> PathBuf { 104 | "out".into() 105 | } 106 | 107 | /// Returns the default 'static/' directory. 108 | fn default_static() -> PathBuf { 109 | "static".into() 110 | } 111 | 112 | /// Returns the default 'layout/' directory. 113 | fn default_layout() -> PathBuf { 114 | "layout".into() 115 | } 116 | } 117 | 118 | impl Default for Dirs { 119 | fn default() -> Self { 120 | Self { 121 | src: "src".into(), 122 | out: "out".into(), 123 | r#static: "static".into(), 124 | layout: "layout".into(), 125 | } 126 | } 127 | } 128 | 129 | /// Loads the `mksite.toml` config file from the current directory. 130 | pub(crate) fn load() -> Result { 131 | let config = fs::read_to_string(FILE_NAME).map_err(|source| Error::Io { 132 | msg: format!("Cannot read {FILE_NAME}"), 133 | source, 134 | })?; 135 | 136 | toml::from_str(&config).map_err(|source| source.into()) 137 | } 138 | 139 | /// Generates the `mksite.toml` config file in the specified directory. 140 | /// `path` must be a directory. 141 | /// 142 | /// The contents of this file are copied verbatim from `mksite.default.toml` 143 | /// via `include_str`. 144 | pub(crate) fn generate(path: &path::Path) -> Result<()> { 145 | let file = fs::OpenOptions::new() 146 | .write(true) 147 | .create_new(true) 148 | .open(path.join(FILE_NAME)); 149 | 150 | let mut file = match file { 151 | Ok(file) => file, 152 | Err(source) => { 153 | return Err(Error::Io { 154 | msg: format!("Cannot create '{}'", path.display()), 155 | source, 156 | }) 157 | } 158 | }; 159 | 160 | file.write_all(include_str!("../mksite.default.toml").as_bytes()) 161 | .map_err(|source| Error::Io { 162 | msg: format!("Cannot write '{}'", path.display()), 163 | source, 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `mksite` 2 | 3 | A file format-agnostic static site generator 4 | 5 | ## Installation 6 | 7 | If you already have [Rust](https://www.rust-lang.org) and [Cargo](https://doc.rust-lang.org/cargo/) installed: 8 | 9 | ```sh 10 | cargo install mksite 11 | ``` 12 | 13 | Alternatively, you can install via git: 14 | 15 | ```sh 16 | cargo install --git https://github.com/alterae/mksite 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```help 22 | mksite 23 | 24 | Commands: 25 | build Build the site according to `mksite.toml` 26 | clean Delete all build outputs 27 | init Initialize a `mksite.toml` file in the current directory 28 | new Scaffold an empty site in a new directory 29 | help Print this message or the help of the given subcommand(s) 30 | 31 | Options: 32 | -q, --quiet Do not print log messages 33 | --log-level What level of logging to enable (error, warn, info, debug, or trace) [default: info] 34 | -h, --help Print help information 35 | -V, --version Print version information 36 | ``` 37 | 38 | `mksite` is a program for turning a tree of text files into a different tree of text files, usually a website. A typical `mksite` project has the following structure: 39 | 40 | - **`mksite.toml`** — The [`mksite.toml` file](#config) is the core of a project, and defines the names of the other significant directories, the data available to templates, and the [transforms](#transforms) to apply to files. 41 | - **`layout/`** _(optional)_ — The `layout/` directory contains [layouts](#layouts), such as html boilerplate, to be applied to files after they are transformed. If you don't want any layouts, or don't want to use `mksite`'s layout system, this folder can be safely omitted. The name of the `layout/` directory can be customized in `mksite.toml`. 42 | - **`out/`** _(generated)_ — The `out/` directory is generated by `mksite` when the site is built, and contains the transformed contents of the `src/` directory, as well as the contents of the `static/` directory, copied as-is. The name of the `out/` directory can be customized in `mksite.toml`. 43 | - **`src/`** — The `src/` directory holds all the non-static source files for the website. Files in `src` must be valid UTF-8 and can contain template expressions using the [Tera](https://tera.netlify.app) templating language. Other than that, they can be anything: LaTex, HTML, Markdown, your own custom markup language, or something else entirely. The name of the `src/` directory can be customized in `mksite.toml`. 44 | - **`static/`** _(optional)_ — The `static/` directory contains static files, such as stylesheets or images, which are copied as-is to the `out/` directory. No templating or transformation occurs. The name of the `static/` directory can be customized in `mksite.toml`. 45 | 46 | ### Config 47 | 48 | An example `mksite.toml` file looks like this: 49 | 50 | ```toml 51 | dirs.src = "src" 52 | dirs.out = "out" 53 | dirs.static = "static" 54 | dirs.layout = "layout" 55 | 56 | [data] 57 | author = { name = "Jane Doe", email = "email@example.com" } 58 | copyright = 2022 59 | 60 | [transforms] 61 | md.html = "pandoc -f markdown -t html" 62 | scd.html = ["scdoc", "pandoc -f man -t html"] 63 | ``` 64 | 65 | The first four lines tell `mksite` the names of the `src/`, `out/`, `static/`, and `layout/` directories, respectively. Changing these will change where `mksite` reads and writes data. For example, `dirs.out = "www"` would cause `mksite` to write the build output in a folder called `www/`. 66 | 67 | Next is the **`data`** section, which is where you can define arbitrary data that will be passed to the template rendering. In templates, this data will be made available under the `data` variable. For details on the template syntax, see the [Tera documentation](https://tera.netlify.app/docs/). 68 | 69 | Finally, we have the **`transforms`** section. _Transforms_ are commands or chains of commands that take a stream of bytes on standard input, and return a stream of bytes for standard output. Transforms can be used to trivially implement many features `mksite` does not natively support, such as markdown rendering and syntax highlighting. The basic syntax of a transform definition is `in.out = "command"` or `in.out = ["command1", "command2", ...]` where `in` is the file extension the transform operates on, `out` is the file extension the transform produces, and `command` is a command to pipe the page through. For more details on the finer points of transforms, see [below](#transforms). 70 | 71 | All fields in this config file are optional. 72 | 73 | ### Layouts 74 | 75 | Layouts are simply Tera templates located in the `layout/` directory that accept an additional `page.content` variable. Layouts must be valid UTF-8, but aside from that they can be any format. 76 | 77 | An example layout file looks like this. 78 | 79 | ```html 80 | 81 | 82 | 83 | 84 | 85 | 86 | {{ page.content | safe }} 87 | 88 | 89 | ``` 90 | 91 | There are two kinds of layouts: default layouts and override layouts: 92 | 93 | _Override_ layouts have the same name, extension, and relative path as a specific file in the `out/` directory, and only apply to that file. For example, the layout `layout/blog/index.html` will only apply to the page `out/blog/index.html`. 94 | 95 | _Default_ layouts have the name `_.ext`, where `ext` is some file extension. Default layouts apply to all files in the corresponding directory and in nested directories that don't have a default layout. For example: 96 | 97 | ``` 98 | layout/ 99 | _.html 100 | blog/ 101 | _.html 102 | index.html 103 | ``` 104 | 105 | In this example, `layout/blog/_.html` will apply to all html files in `out/` _except_ `index.html`, and `layout/_.html` will apply to every html file in `out/` _except_ the contents of the `blog` directory. 106 | 107 | > **Note** 108 | > Layouts are applied _after_ transforms, based on the file extension of the transform output. As a result, a layout like `_.html` will apply to _all_ generated html files, regardless of whether these html files were hand-written or generated from markdown or something else via a transform. 109 | 110 | If no applicable layouts are found for a file, or if there is no `layout/` no layout will be applied. 111 | 112 | If an applicable layout exists, but you would like to prevent it from being applied to a file or folder, you can define an "empty" layout like so: 113 | 114 | ``` 115 | {{ page.content | safe }} 116 | ``` 117 | 118 | ### Transforms 119 | 120 | A transform has an _input extension_, an _output extension_, and a _command_ or _chain_ of commands. 121 | 122 | Each page in `src/` with a file extension matching a transform's input extension is piped into the transform as a stream of bytes, and the resulting stream of bytes is written to a file in the `out/` directory with the same name and path, and a file extension matching the transform's output extension. _Each page can be piped to multiple transforms, and multiple transforms can output the same format._ The relationship between inputs and outputs is many-to-many. 123 | 124 | > **Warning** 125 | > It is important to note that the inputs and outputs of transforms are _streams of arbitrary bytes_, not necessarily valid UTF-8 strings. This is important for interfacing with external non-rust tools, but there are some caveats: 126 | > 127 | > Though this may change in the future, at present all templates and layouts _must_ be valid UTF-8. This means that while transforms can both input and output arbitrary bytes, the original input to a transform (a file in the `src/` directory) will be UTF-8. Additionally, layouts for non-UTF-8 files **are not supported**, and attempting to define a layout for, say, a `.pdf` file will result in an error. 128 | 129 | If a transform has a single command, pages are piped to that command, and the output of the command is written to the output file. For example, these transforms: 130 | 131 | ```toml 132 | [transforms] 133 | md.html = "pandoc -f markdown" 134 | md.pdf = "pandoc -f markdown -t pdf" 135 | ``` 136 | 137 | Will use [pandoc](https://pandoc.org) to produce an html file and a pdf document in `out/` for every markdown file in `src/`. 138 | 139 | If a transform has a chain of commands, pages are piped to each command in the chain in order, and the ouput of the last command is written to the output file, like with shell pipelines. For example: 140 | 141 | ```toml 142 | [transforms] 143 | scd.html = ["scdoc", "pandoc -f man"] 144 | ``` 145 | 146 | Will use [`scdoc`](https://git.sr.ht/~sircmpwn/scdoc) to generate a man page from each `.scd` file, and immediately pipe that man page to `pandoc` to convert it to html. 147 | 148 | ## Contributing 149 | 150 | Pull requests and issues are welcome, but please ensure you run `cargo fmt` before submitting a PR. 151 | 152 | ## Example 153 | 154 | See the [`docs/`](./docs/) folder for an example of a website built using `mksite`. 155 | 156 | ## License 157 | 158 | `mksite` is licensed under the [MIT License](./LICENSE). 159 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | # `mksite` {-} 2 | 3 | A file format-agnostic static site generator 4 | 5 | ## Installation 6 | 7 | If you already have [Rust](https://www.rust-lang.org) and [Cargo](https://doc.rust-lang.org/cargo/) installed: 8 | 9 | ```sh 10 | cargo install mksite 11 | ``` 12 | 13 | Alternatively, you can install via git: 14 | 15 | ```sh 16 | cargo install --git {{ data.github }} 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```help 22 | mksite 23 | 24 | Commands: 25 | build Build the site according to `mksite.toml` 26 | clean Delete all build outputs 27 | init Initialize a `mksite.toml` file in the current directory 28 | new Scaffold an empty site in a new directory 29 | help Print this message or the help of the given subcommand(s) 30 | 31 | Options: 32 | -q, --quiet Do not print log messages 33 | --log-level What level of logging to enable (error, warn, info, debug, or trace) [default: info] 34 | -h, --help Print help information 35 | -V, --version Print version information 36 | ``` 37 | 38 | `mksite` is a program for turning a tree of text files into a different tree of text files, usually a website. A typical `mksite` project has the following structure: 39 | 40 | - **`mksite.toml`** — The [`mksite.toml` file](#config) is the core of a project, and defines the names of the other significant directories, the data available to templates, and the [transforms](#transforms) to apply to files. 41 | - **`layout/`** _(optional)_ — The `layout/` directory contains [layouts](#layouts), such as html boilerplate, to be applied to files after they are transformed. If you don't want any layouts, or don't want to use `mksite`'s layout system, this folder can be safely omitted. The name of the `layout/` directory can be customized in `mksite.toml`. 42 | - **`out/`** _(generated)_ — The `out/` directory is generated by `mksite` when the site is built, and contains the transformed contents of the `src/` directory, as well as the contents of the `static/` directory, copied as-is. The name of the `out/` directory can be customized in `mksite.toml`. 43 | - **`src/`** — The `src/` directory holds all the non-static source files for the website. Files in `src` must be valid UTF-8 and can contain template expressions using the [Tera](https://tera.netlify.app) templating language. Other than that, they can be anything: LaTex, HTML, Markdown, your own custom markup language, or something else entirely. The name of the `src/` directory can be customized in `mksite.toml`. 44 | - **`static/`** _(optional)_ — The `static/` directory contains static files, such as stylesheets or images, which are copied as-is to the `out/` directory. No templating or transformation occurs. The name of the `static/` directory can be customized in `mksite.toml`. 45 | 46 | ### Config 47 | 48 | An example `mksite.toml` file looks like this: 49 | 50 | ```toml 51 | dirs.src = "src" 52 | dirs.out = "out" 53 | dirs.static = "static" 54 | dirs.layout = "layout" 55 | 56 | [data] 57 | author = { name = "Jane Doe", email = "email@example.com" } 58 | copyright = 2022 59 | 60 | [transforms] 61 | md.html = "pandoc -f markdown -t html" 62 | scd.html = ["scdoc", "pandoc -f man -t html"] 63 | ``` 64 | 65 | The first four lines tell `mksite` the names of the `src/`, `out/`, `static/`, and `layout/` directories, respectively. Changing these will change where `mksite` reads and writes data. For example, `dirs.out = "www"` would cause `mksite` to write the build output in a folder called `www/`. 66 | 67 | Next is the **`data`** section, which is where you can define arbitrary data that will be passed to the template rendering. In templates, this data will be made available under the `data` variable. For details on the template syntax, see the [Tera documentation](https://tera.netlify.app/docs/). 68 | 69 | Finally, we have the **`transforms`** section. _Transforms_ are commands or chains of commands that take a stream of bytes on standard input, and return a stream of bytes for standard output. Transforms can be used to trivially implement many features `mksite` does not natively support, such as markdown rendering and syntax highlighting. The basic syntax of a transform definition is `in.out = "command"` or `in.out = ["command1", "command2", ...]` where `in` is the file extension the transform operates on, `out` is the file extension the transform produces, and `command` is a command to pipe the page through. For more details on the finer points of transforms, see [below](#transforms). 70 | 71 | All fields in this config file are optional. 72 | 73 | ### Layouts 74 | 75 | Layouts are simply Tera templates located in the `layout/` directory that accept an additional `content` variable. Layouts must be valid UTF-8, but aside from that they can be any format. 76 | 77 | An example layout file looks like this. 78 | {% raw %} 79 | 80 | ```html 81 | 82 | 83 | 84 | 85 | 86 | 87 | {{ page.content | safe }} 88 | 89 | 90 | ``` 91 | 92 | {%endraw%} 93 | There are two kinds of layouts: default layouts and override layouts: 94 | 95 | _Override_ layouts have the same name, extension, and relative path as a specific file in the `out/` directory, and only apply to that file. For example, the layout `layout/blog/index.html` will only apply to the page `out/blog/index.html`. 96 | 97 | _Default_ layouts have the name `_.ext`, where `ext` is some file extension. Default layouts apply to all files in the corresponding directory and in nested directories that don't have a default layout. For example: 98 | 99 | ``` 100 | layout/ 101 | _.html 102 | blog/ 103 | _.html 104 | index.html 105 | ``` 106 | 107 | In this example, `layout/blog/_.html` will apply to all html files in `out/` _except_ `index.html`, and `layout/_.html` will apply to every html file in `out/` _except_ the contents of the `blog` directory. 108 | 109 | > **Note** 110 | > 111 | > Layouts are applied _after_ transforms, based on the file extension of the transform output. As a result, a layout like `_.html` will apply to _all_ generated html files, regardless of whether these html files were hand-written or generated from markdown or something else via a transform. 112 | 113 | If no applicable layouts are found for a file, or if there is no `layout/` no layout will be applied. 114 | 115 | If an applicable layout exists, but you would like to prevent it from being applied to a file or folder, you can define an "empty" layout like so: 116 | {% raw %} 117 | 118 | ``` 119 | {{ page.content | safe }} 120 | ``` 121 | 122 | {% endraw %} 123 | 124 | ### Transforms 125 | 126 | A transform has an _input extension_, an _output extension_, and a _command_ or _chain_ of commands. 127 | 128 | Each page in `src/` with a file extension matching a transform's input extension is piped into the transform as a stream of bytes, and the resulting stream of bytes is written to a file in the `out/` directory with the same name and path, and a file extension matching the transform's output extension. _Each page can be piped to multiple transforms, and multiple transforms can output the same format._ The relationship between inputs and outputs is many-to-many. 129 | 130 | > **Warning** 131 | > 132 | > It is important to note that the inputs and outputs of transforms are _streams of arbitrary bytes_, not necessarily valid UTF-8 strings. This is important for interfacing with external non-rust tools, but there are some caveats: 133 | > 134 | > Though this may change in the future, at present all templates and layouts _must_ be valid UTF-8. This means that while transforms can both input and output arbitrary bytes, the original input to a transform (a file in the `src/` directory) will be UTF-8. Additionally, layouts for non-UTF-8 files **are not supported**, and attempting to define a layout for, say, a `.pdf` file will result in an error. 135 | 136 | If a transform has a single command, pages are piped to that command, and the output of the command is written to the output file. For example, these transforms: 137 | 138 | ```toml 139 | [transforms] 140 | md.html = "pandoc -f markdown" 141 | md.pdf = "pandoc -f markdown -t pdf" 142 | ``` 143 | 144 | Will use [pandoc](https://pandoc.org) to produce an html file and a pdf document in `out/` for every markdown file in `src/`. 145 | 146 | If a transform has a chain of commands, pages are piped to each command in the chain in order, and the ouput of the last command is written to the output file, like with shell pipelines. For example: 147 | 148 | ```toml 149 | [transforms] 150 | scd.html = ["scdoc", "pandoc -f man"] 151 | ``` 152 | 153 | Will use [`scdoc`](https://git.sr.ht/~sircmpwn/scdoc) to generate a man page from each `.scd` file, and immediately pipe that man page to `pandoc` to convert it to html. 154 | 155 | ## Contributing 156 | 157 | Pull requests and issues are welcome, but please ensure you run `cargo fmt` before submitting a PR. 158 | 159 | ## Example 160 | 161 | See the [source code for this website]({{ data.github }}/tree/main/docs). 162 | 163 | ## License 164 | 165 | `mksite` is licensed under the [MIT License](./LICENSE). 166 | -------------------------------------------------------------------------------- /src/site.rs: -------------------------------------------------------------------------------- 1 | //! Types and methods for modeling and building the website. 2 | 3 | use std::{ffi::OsStr, fs, path::PathBuf}; 4 | 5 | use crate::{config, transform, util, Error, Result}; 6 | 7 | /// Structure representing the site as a whole, containing all the pages and 8 | /// layouts, the site configuration, and the templating engine. 9 | pub(crate) struct Site { 10 | /// The site configuration defined in the `mksite.toml` file. 11 | config: config::Config, 12 | 13 | /// The rendering engine for all templating and layouts. 14 | tera: tera::Tera, 15 | 16 | /// The paths of all the in the source directory. 17 | sources: Vec, 18 | 19 | /// The paths of all the layouts to use, if the layouts directory exists. 20 | layouts: Option>, 21 | 22 | /// List of mappings from sources to outputs. 23 | mappings: Vec, 24 | } 25 | 26 | impl Site { 27 | /// Constructs a new site using the information in the given config. 28 | pub fn new(config: config::Config) -> Result { 29 | Ok(Self { 30 | config: config.clone(), 31 | tera: tera::Tera::default(), 32 | sources: util::walk_dir(&config.dirs.src)?, 33 | mappings: Vec::new(), 34 | layouts: if config.dirs.layout.exists() { 35 | Some(util::walk_dir(&config.dirs.layout)?) 36 | } else { 37 | None 38 | }, 39 | }) 40 | } 41 | 42 | /// Builds templates, renders them, applies transforms and layouts, and 43 | /// copies the results to the configured output directory. 44 | pub fn build(mut self) -> Result<()> { 45 | self.build_templates()?; 46 | let rendered_pages = self.render_pages()?; 47 | self.prepare_mappings(rendered_pages)?; 48 | self.apply_transforms()?; 49 | self.apply_layouts_and_write_output()?; 50 | self.copy_statics() 51 | } 52 | 53 | /// Builds (but does not render) Tera templates for the site. 54 | fn build_templates(&mut self) -> Result<()> { 55 | // build page templates 56 | let dir = &self.config.dirs.src; 57 | 58 | log::debug!("Building page templates"); 59 | self.tera 60 | .add_template_files(util::walk_dir(dir)?.iter().map(|p| (p, None::)))?; 61 | 62 | // build layout templates if they exist 63 | if self.config.dirs.layout.exists() { 64 | log::debug!("Building layout templates"); 65 | let dir = &self.config.dirs.layout; 66 | 67 | self.tera 68 | .add_template_files(util::walk_dir(dir)?.iter().map(|p| (p, None::)))?; 69 | } else { 70 | log::info!( 71 | "Layout directory '{}' does not exist. Layout step will be skipped", 72 | self.config.dirs.layout.display() 73 | ) 74 | } 75 | 76 | log::debug!( 77 | "Added templates: {:#?}", 78 | self.tera.get_template_names().collect::>() 79 | ); 80 | 81 | Ok(()) 82 | } 83 | 84 | /// Renders tera templates to produce [Page]s in preparation for transforms and layouts. 85 | fn render_pages(&mut self) -> Result)>> { 86 | let mut context = tera::Context::new(); 87 | context.insert("data", &self.config.data); 88 | 89 | // render page contents 90 | self.render_page_templates(context) 91 | } 92 | 93 | /// Prepares the mappings required for each [Page] based on transform configurations. 94 | fn prepare_mappings(&mut self, rendered_pages: Vec<(PathBuf, Vec)>) -> Result<()> { 95 | for (source, content) in rendered_pages { 96 | let mut destination = 97 | util::swap_prefix(&source, &self.config.dirs.src, &self.config.dirs.out)?; 98 | 99 | if self.config.ignores.transform.contains(&destination) { 100 | log::info!( 101 | "Skipping transform step for '{}' as it is in the transform ignore list", 102 | destination.display() 103 | ); 104 | } 105 | 106 | match source.extension().and_then(OsStr::to_str) { 107 | Some(ext) 108 | if self.config.transforms.contains_key(ext) 109 | && !self.config.ignores.transform.contains(&destination) => 110 | { 111 | log::debug!("Transforms apply to source '{}'", source.display()); 112 | 113 | for (target_ext, transform) in &self.config.transforms[ext] { 114 | destination.set_extension(target_ext); 115 | 116 | log::debug!( 117 | "Mapping '{}' -> '{}' via {transform}", 118 | source.display(), 119 | destination.display() 120 | ); 121 | 122 | self.mappings.push(Mapping { 123 | source: source.to_owned(), 124 | destination: destination.clone(), 125 | transform: Some(transform.to_owned()), 126 | content: content.to_owned(), 127 | }) 128 | } 129 | } 130 | _ => { 131 | log::debug!("No transforms apply to source 'src/index.md'"); 132 | 133 | log::debug!( 134 | "Mapping '{}' -> '{}'", 135 | source.display(), 136 | destination.display() 137 | ); 138 | 139 | self.mappings.push(Mapping { 140 | source: source.to_owned(), 141 | destination, 142 | transform: None, 143 | content, 144 | }); 145 | } 146 | } 147 | } 148 | 149 | log::debug!( 150 | "Mapped {} page{}", 151 | self.mappings.len(), 152 | if self.mappings.len() != 1 { "s" } else { "" } 153 | ); 154 | 155 | Ok(()) 156 | } 157 | 158 | /// Renders all page templates and returns their contents as byte vecs 159 | /// (except pages in the templating ignore list, which are simply read and 160 | /// returned). 161 | fn render_page_templates(&self, mut context: tera::Context) -> Result)>> { 162 | let mut res = Vec::new(); 163 | 164 | for path in &self.sources { 165 | if !self.config.ignores.template.contains(path) { 166 | // we can render this template 167 | log::info!("Rendering '{}'", path.display()); 168 | 169 | let template_name = path 170 | .to_str() 171 | .ok_or_else(|| Error::PathConversion(path.to_path_buf()))?; 172 | 173 | // TODO: flesh out this hashmap 174 | context.insert("page", &maplit::hashmap! {"source" => path}); 175 | 176 | log::debug!("Using page rendering {context:#?}"); 177 | 178 | res.push(( 179 | path.to_owned(), 180 | self.tera.render(template_name, &context)?.into_bytes(), 181 | )) 182 | } else { 183 | log::info!( 184 | "Skipping template rendering for '{}' as it is in the template ignore list", 185 | path.display() 186 | ); 187 | 188 | // since we didn't render anything, we just grab the file 189 | // contents directly 190 | res.push(( 191 | path.to_owned(), 192 | fs::read(path).map_err(|source| Error::Io { 193 | msg: format!("Could not read '{}'", path.display()), 194 | source, 195 | })?, 196 | )) 197 | } 198 | } 199 | Ok(res) 200 | } 201 | 202 | /// Apply layouts and write the generated files. 203 | fn apply_layouts_and_write_output(&self) -> Result<()> { 204 | for mapping in &self.mappings { 205 | let layout = self.find_layout(mapping)?; 206 | 207 | let output = match layout { 208 | // if there's no layout to apply, just transform the mapping's 209 | // content and use that 210 | None => mapping.content.clone(), 211 | 212 | // if there is a layout, apply it 213 | Some(layout) => { 214 | log::info!( 215 | "Applying layout '{}' to '{}'", 216 | layout.display(), 217 | mapping.destination.display() 218 | ); 219 | 220 | let mut context = tera::Context::new(); 221 | context.insert("data", &self.config.data); 222 | 223 | log::debug!( 224 | "NOTE: Context field `page.content` is omitted from debug output\nUsing layout rendering {context:#?}" 225 | ); 226 | 227 | context.insert( 228 | "page", 229 | &maplit::hashmap! { 230 | "content" => String::from_utf8(mapping.content.clone()) 231 | .map_err(|source| Error::FromUtf8 { 232 | msg: format!( 233 | "Cannot apply layout '{}' to '{}'", 234 | layout.display(), 235 | mapping.destination.display() 236 | ), 237 | source, 238 | })?, 239 | // FIXME: replace this unwrap with better code 240 | "source_path" => mapping.source.to_str().unwrap().to_owned()}, 241 | ); 242 | 243 | let layout_name = layout 244 | .to_str() 245 | .ok_or_else(|| Error::PathConversion(layout.to_path_buf()))?; 246 | 247 | self.tera.render(layout_name, &context)?.into_bytes() 248 | } 249 | }; 250 | 251 | log::info!("Writing '{}'", mapping.destination.display()); 252 | 253 | if let Some(p) = mapping.destination.parent() { 254 | fs::create_dir_all(p).map_err(|source| Error::Io { 255 | msg: format!("Cannot create '{}'", p.display()), 256 | source, 257 | })?; 258 | } 259 | 260 | fs::write(&mapping.destination, output).map_err(|source| Error::Io { 261 | msg: format!("Cannot write '{}'", mapping.destination.display()), 262 | source, 263 | })?; 264 | } 265 | 266 | Ok(()) 267 | } 268 | 269 | /// Returns the path to the applicable layout for a Mapping, if one exists. 270 | fn find_layout(&self, mapping: &Mapping) -> Result> { 271 | if self.config.ignores.layout.contains(&mapping.destination) { 272 | log::info!( 273 | "Skipping layout for '{}' as it is in the layout ignore list", 274 | mapping.destination.display() 275 | ); 276 | 277 | return Ok(None); 278 | } 279 | 280 | match &self.layouts { 281 | None => { 282 | // just do nothing if there's no layout folder 283 | log::debug!( 284 | "Skipping layout for {} as layout directory {} does not exist", 285 | mapping.destination.display(), 286 | self.config.dirs.layout.display() 287 | ); 288 | Ok(None) 289 | } 290 | 291 | Some(layouts) => { 292 | // if there is a layout folder, look for an applicable layout 293 | 294 | // start with the corresponding path 295 | let layout_path = util::swap_prefix( 296 | &mapping.destination, 297 | &self.config.dirs.out, 298 | &self.config.dirs.layout, 299 | )?; 300 | 301 | // if that doesn't exist we'd better go looking for it 302 | if !layouts.contains(&layout_path) { 303 | log::debug!( 304 | "Exact layout match for '{}' not found", 305 | mapping.destination.display() 306 | ); 307 | 308 | // all this work to concatenate a file extension with an 309 | // underscore :/ 310 | let wildcard = "_".to_owned() 311 | + &match layout_path.extension() { 312 | None => "".to_owned(), 313 | Some(ext) => { 314 | if let Some(ext) = ext.to_str() { 315 | ".".to_owned() + ext 316 | } else { 317 | "".to_owned() 318 | } 319 | } 320 | }; 321 | 322 | // iterate up the directory tree until we find a matching 323 | // wildcard layout 324 | for ancestor in layout_path.ancestors() { 325 | log::debug!( 326 | "Searching for wildcard layout '{}' in '{}/'", 327 | wildcard, 328 | ancestor.display() 329 | ); 330 | let layout_path = ancestor.join(&wildcard); 331 | if layouts.contains(&layout_path) { 332 | log::debug!("Found layout '{}'", layout_path.display()); 333 | return Ok(Some(layout_path)); 334 | } 335 | } 336 | } 337 | 338 | // If we get here we didn't find it. 339 | Ok(None) 340 | } 341 | } 342 | } 343 | 344 | /// Applies the transforms for every mapping, mutating them. 345 | fn apply_transforms(&mut self) -> Result<()> { 346 | for mapping in &mut self.mappings { 347 | mapping.transform()?; 348 | } 349 | 350 | Ok(()) 351 | } 352 | 353 | /// Copies the contents of the static dir to the output dir. 354 | fn copy_statics(&self) -> Result<()> { 355 | for asset in util::walk_dir(&self.config.dirs.r#static)? { 356 | let destination = 357 | util::swap_prefix(&asset, &self.config.dirs.r#static, &self.config.dirs.out)?; 358 | 359 | if destination.exists() { 360 | log::debug!( 361 | "'{}' already exists and will be overwritten.", 362 | destination.display() 363 | ); 364 | } 365 | 366 | log::info!( 367 | "Copying '{}' to '{}'", 368 | asset.display(), 369 | destination.display() 370 | ); 371 | 372 | if let Some(p) = destination.parent() { 373 | fs::create_dir_all(p).map_err(|source| Error::Io { 374 | msg: format!("Cannot create '{}'", p.display()), 375 | source, 376 | })?; 377 | } 378 | 379 | fs::copy(&asset, &destination).map_err(|source| Error::Io { 380 | msg: format!( 381 | "Cannot copy static asset '{}' to '{}'", 382 | asset.display(), 383 | destination.display() 384 | ), 385 | source, 386 | })?; 387 | } 388 | 389 | Ok(()) 390 | } 391 | } 392 | 393 | /// Maps a rendered source template to a destination page via a transform. 394 | #[derive(serde::Serialize)] 395 | struct Mapping { 396 | /// The path to the source file this page was generated from, relative to 397 | /// the project root (eg `src/index.md`). 398 | source: PathBuf, 399 | 400 | /// The path this page will be written to, relative to the project root 401 | /// (eg `out/index.html`). 402 | destination: PathBuf, 403 | 404 | /// The transform to apply to this page, if any is applicable. 405 | transform: Option, 406 | 407 | /// The contents of this page after templating. Stored as a byte vec so we 408 | /// can handle non-UTF-8 inputs via the ignore list. 409 | content: Vec, 410 | } 411 | 412 | impl Mapping { 413 | /// Applies this mapping's transform to its content, if one applies. 414 | pub fn transform(&mut self) -> Result<()> { 415 | if let Some(transform) = &self.transform { 416 | log::info!( 417 | "Applying transform {} to '{}'", 418 | transform, 419 | self.destination.display() 420 | ); 421 | 422 | self.content = transform.apply(&self.content)?; 423 | }; 424 | 425 | Ok(()) 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 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-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.15" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.8" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.5" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 64 | dependencies = [ 65 | "windows-sys 0.52.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.4" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.52.0", 76 | ] 77 | 78 | [[package]] 79 | name = "autocfg" 80 | version = "1.3.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 83 | 84 | [[package]] 85 | name = "bitflags" 86 | version = "2.6.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 89 | 90 | [[package]] 91 | name = "block-buffer" 92 | version = "0.10.4" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 95 | dependencies = [ 96 | "generic-array", 97 | ] 98 | 99 | [[package]] 100 | name = "bstr" 101 | version = "1.10.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 104 | dependencies = [ 105 | "memchr", 106 | "serde", 107 | ] 108 | 109 | [[package]] 110 | name = "bumpalo" 111 | version = "3.16.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 114 | 115 | [[package]] 116 | name = "byteorder" 117 | version = "1.5.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 120 | 121 | [[package]] 122 | name = "cc" 123 | version = "1.1.21" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" 126 | dependencies = [ 127 | "shlex", 128 | ] 129 | 130 | [[package]] 131 | name = "cfg-if" 132 | version = "1.0.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 135 | 136 | [[package]] 137 | name = "chrono" 138 | version = "0.4.38" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 141 | dependencies = [ 142 | "android-tzdata", 143 | "iana-time-zone", 144 | "num-traits", 145 | "windows-targets 0.52.6", 146 | ] 147 | 148 | [[package]] 149 | name = "chrono-tz" 150 | version = "0.9.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" 153 | dependencies = [ 154 | "chrono", 155 | "chrono-tz-build", 156 | "phf", 157 | ] 158 | 159 | [[package]] 160 | name = "chrono-tz-build" 161 | version = "0.3.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" 164 | dependencies = [ 165 | "parse-zoneinfo", 166 | "phf", 167 | "phf_codegen", 168 | ] 169 | 170 | [[package]] 171 | name = "clap" 172 | version = "4.5.18" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" 175 | dependencies = [ 176 | "clap_builder", 177 | "clap_derive", 178 | ] 179 | 180 | [[package]] 181 | name = "clap_builder" 182 | version = "4.5.18" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" 185 | dependencies = [ 186 | "anstream", 187 | "anstyle", 188 | "clap_lex", 189 | "strsim", 190 | ] 191 | 192 | [[package]] 193 | name = "clap_derive" 194 | version = "4.5.18" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 197 | dependencies = [ 198 | "heck", 199 | "proc-macro2", 200 | "quote", 201 | "syn", 202 | ] 203 | 204 | [[package]] 205 | name = "clap_lex" 206 | version = "0.7.2" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 209 | 210 | [[package]] 211 | name = "colorchoice" 212 | version = "1.0.2" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 215 | 216 | [[package]] 217 | name = "colored" 218 | version = "2.1.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 221 | dependencies = [ 222 | "lazy_static", 223 | "windows-sys 0.48.0", 224 | ] 225 | 226 | [[package]] 227 | name = "core-foundation-sys" 228 | version = "0.8.7" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 231 | 232 | [[package]] 233 | name = "cpufeatures" 234 | version = "0.2.14" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" 237 | dependencies = [ 238 | "libc", 239 | ] 240 | 241 | [[package]] 242 | name = "crossbeam-deque" 243 | version = "0.8.5" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 246 | dependencies = [ 247 | "crossbeam-epoch", 248 | "crossbeam-utils", 249 | ] 250 | 251 | [[package]] 252 | name = "crossbeam-epoch" 253 | version = "0.9.18" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 256 | dependencies = [ 257 | "crossbeam-utils", 258 | ] 259 | 260 | [[package]] 261 | name = "crossbeam-utils" 262 | version = "0.8.20" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 265 | 266 | [[package]] 267 | name = "crypto-common" 268 | version = "0.1.6" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 271 | dependencies = [ 272 | "generic-array", 273 | "typenum", 274 | ] 275 | 276 | [[package]] 277 | name = "deunicode" 278 | version = "1.6.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" 281 | 282 | [[package]] 283 | name = "digest" 284 | version = "0.10.7" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 287 | dependencies = [ 288 | "block-buffer", 289 | "crypto-common", 290 | ] 291 | 292 | [[package]] 293 | name = "equivalent" 294 | version = "1.0.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 297 | 298 | [[package]] 299 | name = "fern" 300 | version = "0.6.2" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" 303 | dependencies = [ 304 | "log", 305 | ] 306 | 307 | [[package]] 308 | name = "generic-array" 309 | version = "0.14.7" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 312 | dependencies = [ 313 | "typenum", 314 | "version_check", 315 | ] 316 | 317 | [[package]] 318 | name = "getrandom" 319 | version = "0.2.15" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 322 | dependencies = [ 323 | "cfg-if", 324 | "libc", 325 | "wasi", 326 | ] 327 | 328 | [[package]] 329 | name = "globset" 330 | version = "0.4.15" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" 333 | dependencies = [ 334 | "aho-corasick", 335 | "bstr", 336 | "log", 337 | "regex-automata", 338 | "regex-syntax", 339 | ] 340 | 341 | [[package]] 342 | name = "globwalk" 343 | version = "0.9.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" 346 | dependencies = [ 347 | "bitflags", 348 | "ignore", 349 | "walkdir", 350 | ] 351 | 352 | [[package]] 353 | name = "hashbrown" 354 | version = "0.14.5" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 357 | 358 | [[package]] 359 | name = "heck" 360 | version = "0.5.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 363 | 364 | [[package]] 365 | name = "humansize" 366 | version = "2.1.3" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" 369 | dependencies = [ 370 | "libm", 371 | ] 372 | 373 | [[package]] 374 | name = "iana-time-zone" 375 | version = "0.1.61" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 378 | dependencies = [ 379 | "android_system_properties", 380 | "core-foundation-sys", 381 | "iana-time-zone-haiku", 382 | "js-sys", 383 | "wasm-bindgen", 384 | "windows-core", 385 | ] 386 | 387 | [[package]] 388 | name = "iana-time-zone-haiku" 389 | version = "0.1.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 392 | dependencies = [ 393 | "cc", 394 | ] 395 | 396 | [[package]] 397 | name = "ignore" 398 | version = "0.4.23" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 401 | dependencies = [ 402 | "crossbeam-deque", 403 | "globset", 404 | "log", 405 | "memchr", 406 | "regex-automata", 407 | "same-file", 408 | "walkdir", 409 | "winapi-util", 410 | ] 411 | 412 | [[package]] 413 | name = "indexmap" 414 | version = "2.5.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" 417 | dependencies = [ 418 | "equivalent", 419 | "hashbrown", 420 | ] 421 | 422 | [[package]] 423 | name = "is_terminal_polyfill" 424 | version = "1.70.1" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 427 | 428 | [[package]] 429 | name = "itoa" 430 | version = "1.0.11" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 433 | 434 | [[package]] 435 | name = "js-sys" 436 | version = "0.3.70" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" 439 | dependencies = [ 440 | "wasm-bindgen", 441 | ] 442 | 443 | [[package]] 444 | name = "lazy_static" 445 | version = "1.5.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 448 | 449 | [[package]] 450 | name = "libc" 451 | version = "0.2.158" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 454 | 455 | [[package]] 456 | name = "libm" 457 | version = "0.2.8" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 460 | 461 | [[package]] 462 | name = "log" 463 | version = "0.4.22" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 466 | 467 | [[package]] 468 | name = "maplit" 469 | version = "1.0.2" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 472 | 473 | [[package]] 474 | name = "memchr" 475 | version = "2.7.4" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 478 | 479 | [[package]] 480 | name = "mksite" 481 | version = "0.1.2" 482 | dependencies = [ 483 | "clap", 484 | "colored", 485 | "fern", 486 | "log", 487 | "maplit", 488 | "serde", 489 | "shell-words", 490 | "tera", 491 | "thiserror", 492 | "toml", 493 | ] 494 | 495 | [[package]] 496 | name = "num-traits" 497 | version = "0.2.19" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 500 | dependencies = [ 501 | "autocfg", 502 | ] 503 | 504 | [[package]] 505 | name = "once_cell" 506 | version = "1.19.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 509 | 510 | [[package]] 511 | name = "parse-zoneinfo" 512 | version = "0.3.1" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" 515 | dependencies = [ 516 | "regex", 517 | ] 518 | 519 | [[package]] 520 | name = "percent-encoding" 521 | version = "2.3.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 524 | 525 | [[package]] 526 | name = "pest" 527 | version = "2.7.13" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" 530 | dependencies = [ 531 | "memchr", 532 | "thiserror", 533 | "ucd-trie", 534 | ] 535 | 536 | [[package]] 537 | name = "pest_derive" 538 | version = "2.7.13" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" 541 | dependencies = [ 542 | "pest", 543 | "pest_generator", 544 | ] 545 | 546 | [[package]] 547 | name = "pest_generator" 548 | version = "2.7.13" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" 551 | dependencies = [ 552 | "pest", 553 | "pest_meta", 554 | "proc-macro2", 555 | "quote", 556 | "syn", 557 | ] 558 | 559 | [[package]] 560 | name = "pest_meta" 561 | version = "2.7.13" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" 564 | dependencies = [ 565 | "once_cell", 566 | "pest", 567 | "sha2", 568 | ] 569 | 570 | [[package]] 571 | name = "phf" 572 | version = "0.11.2" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 575 | dependencies = [ 576 | "phf_shared", 577 | ] 578 | 579 | [[package]] 580 | name = "phf_codegen" 581 | version = "0.11.2" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" 584 | dependencies = [ 585 | "phf_generator", 586 | "phf_shared", 587 | ] 588 | 589 | [[package]] 590 | name = "phf_generator" 591 | version = "0.11.2" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 594 | dependencies = [ 595 | "phf_shared", 596 | "rand", 597 | ] 598 | 599 | [[package]] 600 | name = "phf_shared" 601 | version = "0.11.2" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 604 | dependencies = [ 605 | "siphasher", 606 | ] 607 | 608 | [[package]] 609 | name = "ppv-lite86" 610 | version = "0.2.20" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 613 | dependencies = [ 614 | "zerocopy", 615 | ] 616 | 617 | [[package]] 618 | name = "proc-macro2" 619 | version = "1.0.86" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 622 | dependencies = [ 623 | "unicode-ident", 624 | ] 625 | 626 | [[package]] 627 | name = "quote" 628 | version = "1.0.37" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 631 | dependencies = [ 632 | "proc-macro2", 633 | ] 634 | 635 | [[package]] 636 | name = "rand" 637 | version = "0.8.5" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 640 | dependencies = [ 641 | "libc", 642 | "rand_chacha", 643 | "rand_core", 644 | ] 645 | 646 | [[package]] 647 | name = "rand_chacha" 648 | version = "0.3.1" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 651 | dependencies = [ 652 | "ppv-lite86", 653 | "rand_core", 654 | ] 655 | 656 | [[package]] 657 | name = "rand_core" 658 | version = "0.6.4" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 661 | dependencies = [ 662 | "getrandom", 663 | ] 664 | 665 | [[package]] 666 | name = "regex" 667 | version = "1.10.6" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 670 | dependencies = [ 671 | "aho-corasick", 672 | "memchr", 673 | "regex-automata", 674 | "regex-syntax", 675 | ] 676 | 677 | [[package]] 678 | name = "regex-automata" 679 | version = "0.4.7" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 682 | dependencies = [ 683 | "aho-corasick", 684 | "memchr", 685 | "regex-syntax", 686 | ] 687 | 688 | [[package]] 689 | name = "regex-syntax" 690 | version = "0.8.4" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 693 | 694 | [[package]] 695 | name = "ryu" 696 | version = "1.0.18" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 699 | 700 | [[package]] 701 | name = "same-file" 702 | version = "1.0.6" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 705 | dependencies = [ 706 | "winapi-util", 707 | ] 708 | 709 | [[package]] 710 | name = "serde" 711 | version = "1.0.210" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 714 | dependencies = [ 715 | "serde_derive", 716 | ] 717 | 718 | [[package]] 719 | name = "serde_derive" 720 | version = "1.0.210" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 723 | dependencies = [ 724 | "proc-macro2", 725 | "quote", 726 | "syn", 727 | ] 728 | 729 | [[package]] 730 | name = "serde_json" 731 | version = "1.0.128" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 734 | dependencies = [ 735 | "itoa", 736 | "memchr", 737 | "ryu", 738 | "serde", 739 | ] 740 | 741 | [[package]] 742 | name = "serde_spanned" 743 | version = "0.6.7" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" 746 | dependencies = [ 747 | "serde", 748 | ] 749 | 750 | [[package]] 751 | name = "sha2" 752 | version = "0.10.8" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 755 | dependencies = [ 756 | "cfg-if", 757 | "cpufeatures", 758 | "digest", 759 | ] 760 | 761 | [[package]] 762 | name = "shell-words" 763 | version = "1.1.0" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 766 | 767 | [[package]] 768 | name = "shlex" 769 | version = "1.3.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 772 | 773 | [[package]] 774 | name = "siphasher" 775 | version = "0.3.11" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 778 | 779 | [[package]] 780 | name = "slug" 781 | version = "0.1.6" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" 784 | dependencies = [ 785 | "deunicode", 786 | "wasm-bindgen", 787 | ] 788 | 789 | [[package]] 790 | name = "strsim" 791 | version = "0.11.1" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 794 | 795 | [[package]] 796 | name = "syn" 797 | version = "2.0.77" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 800 | dependencies = [ 801 | "proc-macro2", 802 | "quote", 803 | "unicode-ident", 804 | ] 805 | 806 | [[package]] 807 | name = "tera" 808 | version = "1.20.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" 811 | dependencies = [ 812 | "chrono", 813 | "chrono-tz", 814 | "globwalk", 815 | "humansize", 816 | "lazy_static", 817 | "percent-encoding", 818 | "pest", 819 | "pest_derive", 820 | "rand", 821 | "regex", 822 | "serde", 823 | "serde_json", 824 | "slug", 825 | "unic-segment", 826 | ] 827 | 828 | [[package]] 829 | name = "thiserror" 830 | version = "1.0.63" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 833 | dependencies = [ 834 | "thiserror-impl", 835 | ] 836 | 837 | [[package]] 838 | name = "thiserror-impl" 839 | version = "1.0.63" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 842 | dependencies = [ 843 | "proc-macro2", 844 | "quote", 845 | "syn", 846 | ] 847 | 848 | [[package]] 849 | name = "toml" 850 | version = "0.8.19" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 853 | dependencies = [ 854 | "serde", 855 | "serde_spanned", 856 | "toml_datetime", 857 | "toml_edit", 858 | ] 859 | 860 | [[package]] 861 | name = "toml_datetime" 862 | version = "0.6.8" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 865 | dependencies = [ 866 | "serde", 867 | ] 868 | 869 | [[package]] 870 | name = "toml_edit" 871 | version = "0.22.21" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" 874 | dependencies = [ 875 | "indexmap", 876 | "serde", 877 | "serde_spanned", 878 | "toml_datetime", 879 | "winnow", 880 | ] 881 | 882 | [[package]] 883 | name = "typenum" 884 | version = "1.17.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 887 | 888 | [[package]] 889 | name = "ucd-trie" 890 | version = "0.1.6" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 893 | 894 | [[package]] 895 | name = "unic-char-property" 896 | version = "0.9.0" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" 899 | dependencies = [ 900 | "unic-char-range", 901 | ] 902 | 903 | [[package]] 904 | name = "unic-char-range" 905 | version = "0.9.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" 908 | 909 | [[package]] 910 | name = "unic-common" 911 | version = "0.9.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" 914 | 915 | [[package]] 916 | name = "unic-segment" 917 | version = "0.9.0" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" 920 | dependencies = [ 921 | "unic-ucd-segment", 922 | ] 923 | 924 | [[package]] 925 | name = "unic-ucd-segment" 926 | version = "0.9.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" 929 | dependencies = [ 930 | "unic-char-property", 931 | "unic-char-range", 932 | "unic-ucd-version", 933 | ] 934 | 935 | [[package]] 936 | name = "unic-ucd-version" 937 | version = "0.9.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" 940 | dependencies = [ 941 | "unic-common", 942 | ] 943 | 944 | [[package]] 945 | name = "unicode-ident" 946 | version = "1.0.13" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 949 | 950 | [[package]] 951 | name = "utf8parse" 952 | version = "0.2.2" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 955 | 956 | [[package]] 957 | name = "version_check" 958 | version = "0.9.5" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 961 | 962 | [[package]] 963 | name = "walkdir" 964 | version = "2.5.0" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 967 | dependencies = [ 968 | "same-file", 969 | "winapi-util", 970 | ] 971 | 972 | [[package]] 973 | name = "wasi" 974 | version = "0.11.0+wasi-snapshot-preview1" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 977 | 978 | [[package]] 979 | name = "wasm-bindgen" 980 | version = "0.2.93" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" 983 | dependencies = [ 984 | "cfg-if", 985 | "once_cell", 986 | "wasm-bindgen-macro", 987 | ] 988 | 989 | [[package]] 990 | name = "wasm-bindgen-backend" 991 | version = "0.2.93" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" 994 | dependencies = [ 995 | "bumpalo", 996 | "log", 997 | "once_cell", 998 | "proc-macro2", 999 | "quote", 1000 | "syn", 1001 | "wasm-bindgen-shared", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "wasm-bindgen-macro" 1006 | version = "0.2.93" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" 1009 | dependencies = [ 1010 | "quote", 1011 | "wasm-bindgen-macro-support", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "wasm-bindgen-macro-support" 1016 | version = "0.2.93" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" 1019 | dependencies = [ 1020 | "proc-macro2", 1021 | "quote", 1022 | "syn", 1023 | "wasm-bindgen-backend", 1024 | "wasm-bindgen-shared", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "wasm-bindgen-shared" 1029 | version = "0.2.93" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" 1032 | 1033 | [[package]] 1034 | name = "winapi-util" 1035 | version = "0.1.9" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1038 | dependencies = [ 1039 | "windows-sys 0.59.0", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "windows-core" 1044 | version = "0.52.0" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1047 | dependencies = [ 1048 | "windows-targets 0.52.6", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "windows-sys" 1053 | version = "0.48.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1056 | dependencies = [ 1057 | "windows-targets 0.48.5", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "windows-sys" 1062 | version = "0.52.0" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1065 | dependencies = [ 1066 | "windows-targets 0.52.6", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "windows-sys" 1071 | version = "0.59.0" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1074 | dependencies = [ 1075 | "windows-targets 0.52.6", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "windows-targets" 1080 | version = "0.48.5" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1083 | dependencies = [ 1084 | "windows_aarch64_gnullvm 0.48.5", 1085 | "windows_aarch64_msvc 0.48.5", 1086 | "windows_i686_gnu 0.48.5", 1087 | "windows_i686_msvc 0.48.5", 1088 | "windows_x86_64_gnu 0.48.5", 1089 | "windows_x86_64_gnullvm 0.48.5", 1090 | "windows_x86_64_msvc 0.48.5", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "windows-targets" 1095 | version = "0.52.6" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1098 | dependencies = [ 1099 | "windows_aarch64_gnullvm 0.52.6", 1100 | "windows_aarch64_msvc 0.52.6", 1101 | "windows_i686_gnu 0.52.6", 1102 | "windows_i686_gnullvm", 1103 | "windows_i686_msvc 0.52.6", 1104 | "windows_x86_64_gnu 0.52.6", 1105 | "windows_x86_64_gnullvm 0.52.6", 1106 | "windows_x86_64_msvc 0.52.6", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "windows_aarch64_gnullvm" 1111 | version = "0.48.5" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1114 | 1115 | [[package]] 1116 | name = "windows_aarch64_gnullvm" 1117 | version = "0.52.6" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1120 | 1121 | [[package]] 1122 | name = "windows_aarch64_msvc" 1123 | version = "0.48.5" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1126 | 1127 | [[package]] 1128 | name = "windows_aarch64_msvc" 1129 | version = "0.52.6" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1132 | 1133 | [[package]] 1134 | name = "windows_i686_gnu" 1135 | version = "0.48.5" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1138 | 1139 | [[package]] 1140 | name = "windows_i686_gnu" 1141 | version = "0.52.6" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1144 | 1145 | [[package]] 1146 | name = "windows_i686_gnullvm" 1147 | version = "0.52.6" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1150 | 1151 | [[package]] 1152 | name = "windows_i686_msvc" 1153 | version = "0.48.5" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1156 | 1157 | [[package]] 1158 | name = "windows_i686_msvc" 1159 | version = "0.52.6" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1162 | 1163 | [[package]] 1164 | name = "windows_x86_64_gnu" 1165 | version = "0.48.5" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1168 | 1169 | [[package]] 1170 | name = "windows_x86_64_gnu" 1171 | version = "0.52.6" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1174 | 1175 | [[package]] 1176 | name = "windows_x86_64_gnullvm" 1177 | version = "0.48.5" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1180 | 1181 | [[package]] 1182 | name = "windows_x86_64_gnullvm" 1183 | version = "0.52.6" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1186 | 1187 | [[package]] 1188 | name = "windows_x86_64_msvc" 1189 | version = "0.48.5" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1192 | 1193 | [[package]] 1194 | name = "windows_x86_64_msvc" 1195 | version = "0.52.6" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1198 | 1199 | [[package]] 1200 | name = "winnow" 1201 | version = "0.6.18" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 1204 | dependencies = [ 1205 | "memchr", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "zerocopy" 1210 | version = "0.7.35" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1213 | dependencies = [ 1214 | "byteorder", 1215 | "zerocopy-derive", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "zerocopy-derive" 1220 | version = "0.7.35" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1223 | dependencies = [ 1224 | "proc-macro2", 1225 | "quote", 1226 | "syn", 1227 | ] 1228 | --------------------------------------------------------------------------------