Getting Started

Preprocessor for mdBook which renders files from a directory as an interactive widget, with syntax highlighting.

Example of mdbook-files

I wrote this to make it easy to showcase examples inside books written using mdBook, which have more than just one file, such as entire Rust crates. The file picker makes it easy to browse around, and the syntax highlighting works just as simple code examples would work.

Install

To install mdbook-files, you have two options. The easiest installation path is to download a prebuilt binary from the GitHub releases. If you have Cargo installed, you can also build it from source.

Prebuilt

You can go to the Releases page and download whichever prebuilt binary is appropriate for the operating system that you are using.

For example, to install it in Linux, you can do like this. You will need to set the MDBOOK_FILES_VERSION to the latest version (or whichever version you want to install).

export MDBOOK_FILES_VERSION=0.1.0
curl -sSL "https://github.com/xfbs/mdbook-files/releases/download/v$MDBOOK_FILES_VERSION/mdbook-files-v$MDBOOK_FILES_VERSION-x86_64-unknown-linux-musl.tar.gz" | sudo tar -C /usr/local/bin -xzv

From Source

Another approach is to install mdbook-files from source. This approach works on operating systems for which no builds exist. To do so, you need to have Cargo1 installed.

cargo install mdbook-files --version 0.1.0

Verify

Once you have installed it, you should be able to verify that you have installed it correctly by running this command:

mdbook-files --version

Which should respond with whichever version you have installed.

1

Use rustup to install it, if neccessary.

Setup

To get started with mdbook-files, the next step is to add it to your mdbook configuration. To do this, you have two options: the automatic install method, and the manual installation method. The former method is recommended, but the latter is still documented here in case you run into issues.

Automatic setup

To install mdbook-files automatically, given that you have already installed it, you can run this command:

mdbook-files install

This will run perform the same steps as the manual installation method.

Manual setup

Next, setup your project by adding this to the configuration:

[preprocessor.files]
prefix = "."

[output.html]
additional-css = ["style.css"]

Next, you need to add the style.css file into your project, by copying it from the repository.

Verify

Once you have done this, you should be able to run mdbook to build your book and not get any issues.

mdbook build

If this succeeds, you are ready for the next step.

Usage

To use mdbook-files, simply drop something like this into your project’s markdown source:

```files
path = "path/to/folder"
```

This will create a widget which renders all of the files in path/to/files.

Reference

This section explains all of the options for configurting mdbook-files and all of the options for the instantiation.

Plugin

The plugin configuration is the settings which are in the book.toml file.

These are all of the options, along with their semantics and default values:

[preprocessor.files]

# specify name of or path to the binary
command = "mdbook-files"

# path prefix added to all invocations
prefix = "."

Files

To render files, a pseudo-code block needs to be added to your markdown source that looks like this:

```files
path = "path/to/folder"
# other config
```

The mkdbook-files plugin will pick up on these and replace them with file widgets.

This section explains the options available for every files instance.

# path to folder to select files to show
path = "path/to/folder"

# Override list for files. Files added here are included even if they are ignored,
# prefixing entries with an exclamation mark turns them into ignores.
files = ["*.png", "!*.md"]

# When set, is the default file to show.
default_file = "README.md"

# Process ignores case insensitively
ignore_case_insensitive = false

# Do not cross file system boundaries.
#
# When this option is enabled, directory traversal will not descend into directories that are
# on a different file system from the root path.
same_file_system = false

# Select the file type given by name.
types = ["png", "rust"]

# Enables ignoring hidden files.
hidden = false

# Whether to follow symbolic links or not.
follow_links = false

# Enables reading `.ignore` files.
#
# `.ignore` files have the same semantics as gitignore files and are supported by search
# tools such as ripgrep and The Silver Searcher.
dot_ignore = false

# Enables reading a global `gitignore` file, whose path is specified in git’s `core.excludesFile`
# config option.
git_global = false

# Enables reading `.git/info/exclude` files.
git_exclude = false

# Enables reading `.gitignore` files.
git_ignore = false

# Whether a git repository is required to apply git-related ignore rules (global rules,
# .gitignore and local exclude rules).
require_git = false

# Enables reading ignore files from parent directories.
git_ignore_parents = false

# Maximum depth to recurse.
#max_depth = 1234

# Ignore files above the specified limit.
#max_filesize = 10000

Examples

Source

Source code of mdbook-files:

  • Cargo.lock
  • Cargo.toml
  • README.md
  • src/
    • lib.rs
    • main.rs
    • options.rs
    • script.js.tera
  • style.css
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
 "memchr",
]

[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"

[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
 "libc",
]

[[package]]
name = "anstream"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6"
dependencies = [
 "anstyle",
 "anstyle-parse",
 "anstyle-query",
 "anstyle-wincon",
 "colorchoice",
 "utf8parse",
]

[[package]]
name = "anstyle"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"

[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
 "utf8parse",
]

[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
 "windows-sys 0.52.0",
]

[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
 "anstyle",
 "windows-sys 0.52.0",
]

[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"

[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"

[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"

[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
 "generic-array",
]

[[package]]
name = "bstr"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
dependencies = [
 "memchr",
 "regex-automata",
 "serde",
]

[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"

[[package]]
name = "camino"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
dependencies = [
 "serde",
]

[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
 "libc",
]

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
 "android-tzdata",
 "iana-time-zone",
 "num-traits",
 "windows-targets 0.48.5",
]

[[package]]
name = "chrono-tz"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e23185c0e21df6ed832a12e2bda87c7d1def6842881fb634a8511ced741b0d76"
dependencies = [
 "chrono",
 "chrono-tz-build",
 "phf",
]

[[package]]
name = "chrono-tz-build"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f"
dependencies = [
 "parse-zoneinfo",
 "phf",
 "phf_codegen",
]

[[package]]
name = "clap"
version = "4.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2"
dependencies = [
 "clap_builder",
 "clap_derive",
]

[[package]]
name = "clap_builder"
version = "4.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb"
dependencies = [
 "anstream",
 "anstyle",
 "clap_lex",
 "strsim",
 "terminal_size",
]

[[package]]
name = "clap_complete"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae"
dependencies = [
 "clap",
]

[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "clap_lex"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"

[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"

[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"

[[package]]
name = "cpufeatures"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
dependencies = [
 "libc",
]

[[package]]
name = "crossbeam-deque"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751"
dependencies = [
 "cfg-if",
 "crossbeam-epoch",
 "crossbeam-utils",
]

[[package]]
name = "crossbeam-epoch"
version = "0.9.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d"
dependencies = [
 "autocfg",
 "cfg-if",
 "crossbeam-utils",
]

[[package]]
name = "crossbeam-utils"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c"
dependencies = [
 "cfg-if",
]

[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
 "generic-array",
 "typenum",
]

[[package]]
name = "deunicode"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a"

[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
 "block-buffer",
 "crypto-common",
]

[[package]]
name = "env_logger"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
 "humantime",
 "is-terminal",
 "log",
 "regex",
 "termcolor",
]

[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
 "libc",
 "windows-sys 0.52.0",
]

[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"

[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
 "typenum",
 "version_check",
]

[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
 "unicode-width",
]

[[package]]
name = "getrandom"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
 "cfg-if",
 "libc",
 "wasi",
]

[[package]]
name = "globset"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
dependencies = [
 "aho-corasick",
 "bstr",
 "log",
 "regex-automata",
 "regex-syntax",
]

[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
 "bitflags 1.3.2",
 "ignore",
 "walkdir",
]

[[package]]
name = "handlebars"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225"
dependencies = [
 "log",
 "pest",
 "pest_derive",
 "serde",
 "serde_json",
 "thiserror",
]

[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"

[[package]]
name = "hermit-abi"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"

[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
 "libm",
]

[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"

[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
 "android_system_properties",
 "core-foundation-sys",
 "iana-time-zone-haiku",
 "js-sys",
 "wasm-bindgen",
 "windows-core",
]

[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
 "cc",
]

[[package]]
name = "ignore"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
dependencies = [
 "crossbeam-deque",
 "globset",
 "log",
 "memchr",
 "regex-automata",
 "same-file",
 "walkdir",
 "winapi-util",
]

[[package]]
name = "is-terminal"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
 "hermit-abi",
 "rustix",
 "windows-sys 0.48.0",
]

[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"

[[package]]
name = "js-sys"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
dependencies = [
 "wasm-bindgen",
]

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "libc"
version = "0.2.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"

[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"

[[package]]
name = "linux-raw-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"

[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"

[[package]]
name = "mdbook"
version = "0.4.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80992cb0e05f22cc052c99f8e883f1593b891014b96a8b4637fd274d7030c85e"
dependencies = [
 "anyhow",
 "chrono",
 "clap",
 "clap_complete",
 "env_logger",
 "handlebars",
 "log",
 "memchr",
 "once_cell",
 "opener",
 "pathdiff",
 "pulldown-cmark",
 "regex",
 "serde",
 "serde_json",
 "shlex",
 "tempfile",
 "toml",
 "topological-sort",
]

[[package]]
name = "mdbook-files"
version = "0.2.0"
dependencies = [
 "anyhow",
 "camino",
 "clap",
 "env_logger",
 "ignore",
 "log",
 "mdbook",
 "pulldown-cmark",
 "pulldown-cmark-to-cmark",
 "serde",
 "serde_json",
 "tera",
 "toml",
 "uuid",
]

[[package]]
name = "memchr"
version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"

[[package]]
name = "normpath"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5"
dependencies = [
 "windows-sys 0.48.0",
]

[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
 "autocfg",
]

[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"

[[package]]
name = "opener"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c62dcb6174f9cb326eac248f07e955d5d559c272730b6c03e396b443b562788"
dependencies = [
 "bstr",
 "normpath",
 "winapi",
]

[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
 "regex",
]

[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"

[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"

[[package]]
name = "pest"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5"
dependencies = [
 "memchr",
 "thiserror",
 "ucd-trie",
]

[[package]]
name = "pest_derive"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2"
dependencies = [
 "pest",
 "pest_generator",
]

[[package]]
name = "pest_generator"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227"
dependencies = [
 "pest",
 "pest_meta",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "pest_meta"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6"
dependencies = [
 "once_cell",
 "pest",
 "sha2",
]

[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
 "phf_shared",
]

[[package]]
name = "phf_codegen"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
 "phf_generator",
 "phf_shared",
]

[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
 "phf_shared",
 "rand",
]

[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
 "siphasher",
]

[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"

[[package]]
name = "proc-macro2"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "pulldown-cmark"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
dependencies = [
 "bitflags 1.3.2",
 "getopts",
 "memchr",
 "unicase",
]

[[package]]
name = "pulldown-cmark-to-cmark"
version = "11.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a883e495f8fc4f209521b03a89dde6f6f211ed7cf92e85693c82508e8b722710"
dependencies = [
 "pulldown-cmark",
]

[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
 "libc",
 "rand_chacha",
 "rand_core",
]

[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
 "ppv-lite86",
 "rand_core",
]

[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
 "getrandom",
]

[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
 "bitflags 1.3.2",
]

[[package]]
name = "regex"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
 "aho-corasick",
 "memchr",
 "regex-automata",
 "regex-syntax",
]

[[package]]
name = "regex-automata"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
 "aho-corasick",
 "memchr",
 "regex-syntax",
]

[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"

[[package]]
name = "rustix"
version = "0.38.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
dependencies = [
 "bitflags 2.4.1",
 "errno",
 "libc",
 "linux-raw-sys",
 "windows-sys 0.52.0",
]

[[package]]
name = "ryu"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"

[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
 "winapi-util",
]

[[package]]
name = "serde"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "serde_json"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
 "cfg-if",
 "cpufeatures",
 "digest",
]

[[package]]
name = "shlex"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"

[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"

[[package]]
name = "slug"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4"
dependencies = [
 "deunicode",
 "wasm-bindgen",
]

[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"

[[package]]
name = "syn"
version = "2.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "tempfile"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
 "cfg-if",
 "fastrand",
 "redox_syscall",
 "rustix",
 "windows-sys 0.48.0",
]

[[package]]
name = "tera"
version = "1.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
dependencies = [
 "chrono",
 "chrono-tz",
 "globwalk",
 "humansize",
 "lazy_static",
 "percent-encoding",
 "pest",
 "pest_derive",
 "rand",
 "regex",
 "serde",
 "serde_json",
 "slug",
 "unic-segment",
]

[[package]]
name = "termcolor"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449"
dependencies = [
 "winapi-util",
]

[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
 "rustix",
 "windows-sys 0.48.0",
]

[[package]]
name = "thiserror"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7"
dependencies = [
 "thiserror-impl",
]

[[package]]
name = "thiserror-impl"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
 "serde",
]

[[package]]
name = "topological-sort"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"

[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"

[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"

[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
 "unic-char-range",
]

[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"

[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"

[[package]]
name = "unic-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
dependencies = [
 "unic-ucd-segment",
]

[[package]]
name = "unic-ucd-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
dependencies = [
 "unic-char-property",
 "unic-char-range",
 "unic-ucd-version",
]

[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
 "unic-common",
]

[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
 "version_check",
]

[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"

[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"

[[package]]
name = "uuid"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
 "getrandom",
 "serde",
]

[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
 "same-file",
 "winapi-util",
]

[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

[[package]]
name = "wasm-bindgen"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
 "cfg-if",
 "wasm-bindgen-macro",
]

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
 "bumpalo",
 "log",
 "once_cell",
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
 "quote",
 "wasm-bindgen-macro-support",
]

[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
 "winapi-i686-pc-windows-gnu",
 "winapi-x86_64-pc-windows-gnu",
]

[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
 "winapi",
]

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
 "windows-targets 0.48.5",
]

[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
 "windows-targets 0.48.5",
]

[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
 "windows-targets 0.52.0",
]

[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
 "windows_aarch64_gnullvm 0.48.5",
 "windows_aarch64_msvc 0.48.5",
 "windows_i686_gnu 0.48.5",
 "windows_i686_msvc 0.48.5",
 "windows_x86_64_gnu 0.48.5",
 "windows_x86_64_gnullvm 0.48.5",
 "windows_x86_64_msvc 0.48.5",
]

[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
 "windows_aarch64_gnullvm 0.52.0",
 "windows_aarch64_msvc 0.52.0",
 "windows_i686_gnu 0.52.0",
 "windows_i686_msvc 0.52.0",
 "windows_x86_64_gnu 0.52.0",
 "windows_x86_64_gnullvm 0.52.0",
 "windows_x86_64_msvc 0.52.0",
]

[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"

[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"

[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"

[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"

[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"

[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"

[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"

[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"

[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"

[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"

[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"

[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"

[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[package]
name = "mdbook-files"
version = "0.2.0"
edition = "2021"
authors = ["Patrick Elsen <pelsen@xfbs.net>"]
license = "MIT"
description = "Preprocessor for mdbook which renders files from a directory as an interactive widget"
repository = "https://github.com/xfbs/mdbook-files"
readme = "README.md"

[dependencies]
anyhow = "1.0.75"
camino = { version = "1.1.6", features = ["serde", "serde1"] }
clap = { version = "4.4.8", features = ["derive"] }
env_logger = "0.10.1"
ignore = "0.4.21"
log = "0.4.20"
mdbook = { version = "0.4.35", default-features = false }
pulldown-cmark = "0.9.3"
pulldown-cmark-to-cmark = "11.0.1"
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
tera = { version = "1.19.1", default-features = false, features = ["builtins"] }
toml = "0.5.11"
uuid = { version = "1.6.1", features = ["v4", "serde"] }
# mdbook-files

[![docs.rs status](https://img.shields.io/docsrs/mdbook-files)](https://docs.rs/mdbook-files)
[![Crates.io version](https://img.shields.io/crates/v/mdbook-files)](https://crates.io/crates/mdbook-files)

Preprocessor for [mdBook][mdBook] which renders files from a directory as an
interactive widget, with syntax highlighting.

![Example of mdbook-files](docs/src/example.png)

## Example

You can run the example by launching `mdbook` in the example directory in this
repository.

```
mdbook serve
```

## Usage

Install `mdbook-files` using `cargo`:

```
cargo install mdbook-files
```

Put the following into your `book.toml`:

```toml
[preprocessor.files]
prefix = "examples"
```

The prefix is a path, relative to which files are to be included.  It is
mandatory to give a prefix. Every include path in the book must be within this
prefix.

You will also need to add the `style.css` from this repository to your list of
extra CSS files:

```toml
[output.html]
additional-css = ["style.css"]
```

To use it, add something like this to your book:

~~~markdown
```files
title = "Files in subfolder"
paths = ["subfolder/**"]
```
~~~

This will produce a widget with all files in `examples/subfolder`, with the given
title. The content of this is a TOML document which contains configuration.

## License

MIT.

[mdBook]: https://github.com/rust-lang/mdBook/
use anyhow::{bail, Context as _, Result};
use camino::Utf8PathBuf;
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use log::*;
use mdbook::{
    book::{Book, Chapter},
    errors::Result as MdbookResult,
    preprocess::{Preprocessor, PreprocessorContext},
    BookItem,
};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use pulldown_cmark_to_cmark::cmark;
use serde::Deserialize;
use std::{collections::BTreeMap, fmt::Write};
use tera::Tera;
use toml::value::Value;
use uuid::Uuid;

/// Configuration for an invocation of files
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Files {
    /// Path to files
    pub path: Utf8PathBuf,

    /// Add a glob to the set of overrides.
    ///
    /// Globs provided here have precisely the same semantics as a single line in a gitignore file,
    /// where the meaning of `!` is inverted: namely, `!` at the beginning of a glob will ignore a
    /// file. Without `!`, all matches of the glob provided are treated as whitelist matches.
    #[serde(default)]
    pub files: Vec<String>,

    /// When specified, path to the file that is opened by default.
    #[serde(default)]
    pub default_file: Option<Utf8PathBuf>,

    /// Process ignores case insensitively
    #[serde(default)]
    pub ignore_case_insensitive: bool,

    /// Do not cross file system boundaries.
    ///
    /// When this option is enabled, directory traversal will not descend into directories that are
    /// on a different file system from the root path.
    #[serde(default)]
    pub same_file_system: bool,

    /// Select the file type given by name.
    #[serde(default)]
    pub types: Vec<String>,

    /// Enables ignoring hidden files.
    #[serde(default)]
    pub hidden: bool,

    /// Whether to follow symbolic links or not.
    #[serde(default)]
    pub follow_links: bool,

    /// Enables reading `.ignore` files.
    ///
    /// `.ignore` files have the same semantics as gitignore files and are supported by search
    /// tools such as ripgrep and The Silver Searcher.
    #[serde(default)]
    pub dot_ignore: bool,

    /// Enables reading a global `gitignore` file, whose path is specified in git’s `core.excludesFile`
    /// config option.
    #[serde(default)]
    pub git_global: bool,

    /// Enables reading `.git/info/exclude` files.
    #[serde(default)]
    pub git_exclude: bool,

    /// Enables reading `.gitignore` files.
    #[serde(default)]
    pub git_ignore: bool,

    /// Whether a git repository is required to apply git-related ignore rules (global rules,
    /// .gitignore and local exclude rules).
    #[serde(default)]
    pub require_git: bool,

    /// Enables reading ignore files from parent directories.
    #[serde(default)]
    pub git_ignore_parents: bool,

    /// The maximum depth to recurse.
    #[serde(default)]
    pub max_depth: Option<usize>,

    /// Whether to ignore files above the specified limit.
    #[serde(default)]
    pub max_filesize: Option<u64>,

    #[serde(default)]
    pub height: Option<String>,
}

/// Configuration for the plugin
#[derive(Deserialize)]
pub struct Config {
    pub prefix: Utf8PathBuf,
}

#[derive(Clone, Debug, Copy)]
pub struct Context<'a> {
    prefix: &'a Utf8PathBuf,
    tera: &'a Tera,
}

pub struct Instance<'a> {
    context: Context<'a>,
    data: Files,
    uuid: Uuid,
}

#[derive(Clone, Debug)]
pub enum TreeNode {
    Directory(BTreeMap<String, TreeNode>),
    File(Uuid),
}

impl Default for TreeNode {
    fn default() -> Self {
        TreeNode::Directory(Default::default())
    }
}

impl TreeNode {
    fn insert(&mut self, path: &[&str], uuid: Uuid) {
        match self {
            TreeNode::Directory(files) if path.len() == 1 => {
                files.insert(path[0].into(), TreeNode::File(uuid));
            }
            TreeNode::Directory(files) => {
                files
                    .entry(path[0].into())
                    .or_default()
                    .insert(&path[1..], uuid);
            }
            TreeNode::File(_file) => panic!("entry exists"),
        }
    }

    pub fn render(&self) -> Result<String> {
        let mut output = String::new();
        match self {
            TreeNode::File(_) => bail!("root node cannot be file"),
            TreeNode::Directory(files) => Self::render_files(&mut output, files)?,
        }
        Ok(output)
    }

    fn render_files(output: &mut dyn Write, files: &BTreeMap<String, TreeNode>) -> Result<()> {
        write!(output, "<ul>")?;
        for (path, node) in files {
            node.render_inner(output, path)?;
        }
        write!(output, "</ul>")?;
        Ok(())
    }

    fn render_inner(&self, output: &mut dyn Write, name: &str) -> Result<()> {
        match self {
            TreeNode::File(uuid) => {
                write!(
                    output,
                    r#"<li id="button-{uuid}" class="mdbook-files-button">{name}</li>"#
                )?;
            }
            TreeNode::Directory(files) => {
                write!(
                    output,
                    r#"<li class="mdbook-files-folder"><span>{name}/</span>"#
                )?;
                Self::render_files(output, files)?;
                write!(output, "</li>")?;
            }
        }
        Ok(())
    }
}

pub type FilesMap = BTreeMap<Utf8PathBuf, Uuid>;

impl<'a> Instance<'a> {
    fn parent(&self) -> Utf8PathBuf {
        self.context.prefix.join(&self.data.path)
    }

    fn files(&self) -> Result<FilesMap> {
        let mut paths: FilesMap = Default::default();
        let parent = self.parent();
        let mut overrides = OverrideBuilder::new(&parent);
        for item in &self.data.files {
            overrides.add(item)?;
        }
        let overrides = overrides.build()?;
        let mut walker = WalkBuilder::new(&parent);
        walker
            .standard_filters(false)
            .ignore_case_insensitive(self.data.ignore_case_insensitive)
            .same_file_system(self.data.same_file_system)
            .require_git(self.data.require_git)
            .hidden(self.data.hidden)
            .ignore(self.data.dot_ignore)
            .git_ignore(self.data.git_ignore)
            .git_exclude(self.data.git_exclude)
            .git_global(self.data.git_global)
            .parents(self.data.git_ignore_parents)
            .follow_links(self.data.follow_links)
            .max_depth(self.data.max_depth)
            .overrides(overrides)
            .max_filesize(self.data.max_filesize);

        let walker = walker.build();

        for path in walker {
            let path = path?;
            if path.file_type().unwrap().is_file() {
                paths.insert(path.path().to_path_buf().try_into()?, Uuid::new_v4());
            }
        }

        info!("Found {} matching files", paths.len());
        if paths.is_empty() {
            bail!("No files matched");
        }

        Ok(paths)
    }

    fn left(&self, files: &FilesMap) -> Result<String> {
        let mut output = String::new();
        let parent = self.parent();
        output.push_str(r#"<div class="mdbook-files-left">"#);

        let mut root = TreeNode::default();
        for (path, uuid) in files.iter() {
            let path = path.strip_prefix(&parent)?;
            let path: Vec<_> = path.components().map(|c| c.as_str()).collect();
            root.insert(&path[..], *uuid);
        }

        let list = root.render()?;
        output.push_str(&list);
        output.push_str("</div>");
        Ok(output)
    }

    fn right(&self, files: &FilesMap) -> Result<Vec<Event<'static>>> {
        let mut events = vec![];
        events.push(Event::Html(CowStr::Boxed(
            r#"<div class="mdbook-files-right">"#.to_string().into(),
        )));

        for (path, uuid) in files {
            info!("Reading {path}");
            let contents = std::fs::read_to_string(path)?;
            let extension = path.extension().unwrap_or("");
            let tag = Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Boxed(extension.into())));

            events.push(Event::Html(CowStr::Boxed(
                format!(r#"<div id="file-{uuid}" class="mdbook-file visible">"#).into(),
            )));

            events.push(Event::Start(tag.clone()));
            events.push(Event::Text(CowStr::Boxed(contents.into())));
            events.push(Event::End(tag));

            events.push(Event::Html(CowStr::Boxed("</div>".to_string().into())));
        }

        events.push(Event::Html(CowStr::Boxed("</div>".to_string().into())));
        Ok(events)
    }

    fn events(&self) -> Result<Vec<Event<'static>>> {
        let paths = self.files()?;

        let mut events = vec![];

        let height = self.data.height.as_deref().unwrap_or("300px");
        events.push(Event::Html(CowStr::Boxed(
            format!(
                r#"<div id="files-{}" class="mdbook-files" style="height: {height};">"#,
                self.uuid
            )
            .into(),
        )));

        events.push(Event::Html(CowStr::Boxed(self.left(&paths)?.into())));
        events.append(&mut self.right(&paths)?);
        events.push(Event::Html(CowStr::Boxed("</div>".to_string().into())));

        let uuids: Vec<Uuid> = paths.values().copied().collect();
        let visible = match &self.data.default_file {
            Some(file) => paths.get(&self.parent().join(file)).unwrap(),
            None => &uuids[0],
        };

        let mut context = tera::Context::new();
        context.insert("uuids", &uuids);
        context.insert("visible", visible);

        let script = self.context.tera.render("script", &context)?;

        events.push(Event::Html(CowStr::Boxed(
            format!("<script>{script}</script>").into(),
        )));

        events.push(Event::HardBreak);
        Ok(events)
    }
}

impl<'b> Context<'b> {
    fn map(&self, book: Book) -> Result<Book> {
        let mut book = book;
        book.sections = std::mem::take(&mut book.sections)
            .into_iter()
            .map(|section| self.map_book_item(section))
            .collect::<Result<_, _>>()?;
        Ok(book)
    }

    fn map_book_item(&self, item: BookItem) -> Result<BookItem> {
        let result = match item {
            BookItem::Chapter(chapter) => BookItem::Chapter(self.map_chapter(chapter)?),
            other => other,
        };

        Ok(result)
    }

    fn map_code(&self, code: CowStr<'_>) -> Result<Vec<Event<'static>>> {
        Instance {
            data: toml::from_str(&code)?,
            uuid: Uuid::new_v4(),
            context: *self,
        }
        .events()
    }

    fn label(&self) -> &str {
        "files"
    }

    fn map_chapter(&self, mut chapter: Chapter) -> Result<Chapter> {
        chapter.content = self.map_markdown(&chapter.content)?;
        chapter.sub_items = std::mem::take(&mut chapter.sub_items)
            .into_iter()
            .map(|item| self.map_book_item(item))
            .collect::<Result<_, _>>()?;
        Ok(chapter)
    }

    fn map_markdown(&self, markdown: &str) -> Result<String> {
        let mut parser = Parser::new_ext(markdown, Options::all());
        let mut events = vec![];

        loop {
            let next = parser.next();
            match next {
                None => break,
                Some(Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(label))))
                    if &*label == self.label() =>
                {
                    let mapped = match parser.next() {
                        Some(Event::Text(code)) => self.map_code(code).context("Mapping code")?,
                        other => unreachable!("Got {other:?}"),
                    };

                    for event in mapped.into_iter() {
                        events.push(event);
                    }

                    parser.next();
                }
                Some(event) => events.push(event),
            }
        }

        let mut buf = String::with_capacity(markdown.len());
        let output = cmark(events.iter(), &mut buf).map(|_| buf)?;
        Ok(output)
    }
}

#[derive(Clone, Debug)]
pub struct FilesPreprocessor {
    templates: Tera,
}

impl Default for FilesPreprocessor {
    fn default() -> Self {
        Self::new()
    }
}

impl FilesPreprocessor {
    pub fn new() -> Self {
        let mut templates = Tera::default();
        templates
            .add_raw_template("script", include_str!("script.js.tera"))
            .unwrap();
        Self { templates }
    }
}

impl Preprocessor for FilesPreprocessor {
    fn name(&self) -> &str {
        "files"
    }

    fn run(&self, ctx: &PreprocessorContext, book: Book) -> MdbookResult<Book> {
        let config = ctx.config.get_preprocessor(self.name()).unwrap();
        let config: Config = Value::Table(config.clone()).try_into().unwrap();
        let instance = Context {
            prefix: &config.prefix,
            tera: &self.templates,
        };
        instance.map(book)
    }
}
use anyhow::{bail, Result};
use clap::Parser;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook_files::FilesPreprocessor;
use options::{Command, Options};
use std::io;

mod options;

impl Options {
    fn run(&self, preprocessor: &dyn Preprocessor) -> Result<()> {
        match &self.command {
            Some(Command::Supports(command)) => {
                if preprocessor.supports_renderer(&command.renderer) {
                    Ok(())
                } else {
                    bail!("unknown renderer {}", command.renderer);
                }
            }
            None | Some(Command::Process) => {
                let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
                let output = preprocessor.run(&ctx, book)?;
                serde_json::to_writer(io::stdout(), &output)?;
                Ok(())
            }
            Some(Command::Install(_command)) => Ok(()),
        }
    }
}

fn main() -> Result<()> {
    env_logger::init();
    let options = options::Options::parse();
    let renderer = FilesPreprocessor::new();
    options.run(&renderer)
}
use clap::Parser;
use std::path::PathBuf;

/// Preprocessor for mdBook which renders files from a directory as an interactive widget, with
/// syntax highlighting.
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct Options {
    #[clap(subcommand)]
    pub command: Option<Command>,
}

#[derive(Parser, Debug)]
pub enum Command {
    /// Check if the renderer is supported.
    Supports(SupportsCommand),
    /// Process a parsed book (default).
    Process,
    /// Install support for mdbook-files into the current mdbook project.
    Install(InstallCommand),
}

#[derive(Parser, Debug)]
pub struct SupportsCommand {
    pub renderer: String,
}

#[derive(Parser, Debug)]
pub struct InstallCommand {
    #[clap(long)]
    pub assets: Option<PathBuf>,
}
window.addEventListener("load", (event) => {
    const uuids = {{ uuids | json_encode() }};
    function set_visible(uuid) {
        uuids.forEach((uuid) => {
            document.getElementById(`button-${uuid}`).classList.remove("active");
            document.getElementById(`file-${uuid}`).classList.remove("visible");
        });
        const button = document.getElementById(`button-${uuid}`).classList.add("active");
        const file = document.getElementById(`file-${uuid}`).classList.add("visible");
    }
    function add_hook(uuid) {
        const button = document.getElementById(`button-${uuid}`);
        button.addEventListener("click", (event) => set_visible(uuid));
    }
    uuids.forEach((uuid) => add_hook(uuid));
    set_visible({{ visible | json_encode() }});
});

/* style for mdbook-files */

.mdbook-files {
    display: flex;
    background-color: var(--sidebar-bg);
    margin: 1em 0;
}

.mdbook-files ul {
    list-style-type: none;
    padding: 2mm;
    margin: 0;
}

.mdbook-files li > ul {
    padding-top: 0;
    padding-bottom: 0;
}

.mdbook-files .mdbook-files-folder > span {
    font-weight: 600;
}

.mdbook-files ul .active {
    font-weight: 600;
}

.mdbook-files ul > li.mdbook-files-button {
    cursor: pointer;
}

.mdbook-files ul > li.mdbook-files-button:hover {
    color: var(--links);
}

.mdbook-files-left {
    width: 150px;
    min-width: 150px;
    overflow: scroll;
}

.mdbook-files-right {
    flex-grow: 1;
    overflow: scroll;
}

.mdbook-file {
    display: none;
}

.mdbook-file pre {
    margin: 0;
    height: 100%;
}

.mdbook-file pre code {
    height: 100%;
}

.mdbook-file.visible {
    display: block;
    height: 100%;
}

This is some paragraph after the thing.

Plugin Source

Source code of mdbook-files:

  • Cargo.lock
  • Cargo.toml
  • README.md
  • src/
    • lib.rs
    • main.rs
    • options.rs
    • script.js.tera
  • style.css
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
 "memchr",
]

[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"

[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
 "libc",
]

[[package]]
name = "anstream"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6"
dependencies = [
 "anstyle",
 "anstyle-parse",
 "anstyle-query",
 "anstyle-wincon",
 "colorchoice",
 "utf8parse",
]

[[package]]
name = "anstyle"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"

[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
 "utf8parse",
]

[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
 "windows-sys 0.52.0",
]

[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
 "anstyle",
 "windows-sys 0.52.0",
]

[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"

[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"

[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"

[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
 "generic-array",
]

[[package]]
name = "bstr"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
dependencies = [
 "memchr",
 "regex-automata",
 "serde",
]

[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"

[[package]]
name = "camino"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
dependencies = [
 "serde",
]

[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
 "libc",
]

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
 "android-tzdata",
 "iana-time-zone",
 "num-traits",
 "windows-targets 0.48.5",
]

[[package]]
name = "chrono-tz"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e23185c0e21df6ed832a12e2bda87c7d1def6842881fb634a8511ced741b0d76"
dependencies = [
 "chrono",
 "chrono-tz-build",
 "phf",
]

[[package]]
name = "chrono-tz-build"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f"
dependencies = [
 "parse-zoneinfo",
 "phf",
 "phf_codegen",
]

[[package]]
name = "clap"
version = "4.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2"
dependencies = [
 "clap_builder",
 "clap_derive",
]

[[package]]
name = "clap_builder"
version = "4.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb"
dependencies = [
 "anstream",
 "anstyle",
 "clap_lex",
 "strsim",
 "terminal_size",
]

[[package]]
name = "clap_complete"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae"
dependencies = [
 "clap",
]

[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "clap_lex"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"

[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"

[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"

[[package]]
name = "cpufeatures"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
dependencies = [
 "libc",
]

[[package]]
name = "crossbeam-deque"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751"
dependencies = [
 "cfg-if",
 "crossbeam-epoch",
 "crossbeam-utils",
]

[[package]]
name = "crossbeam-epoch"
version = "0.9.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d"
dependencies = [
 "autocfg",
 "cfg-if",
 "crossbeam-utils",
]

[[package]]
name = "crossbeam-utils"
version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c"
dependencies = [
 "cfg-if",
]

[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
 "generic-array",
 "typenum",
]

[[package]]
name = "deunicode"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a"

[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
 "block-buffer",
 "crypto-common",
]

[[package]]
name = "env_logger"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
 "humantime",
 "is-terminal",
 "log",
 "regex",
 "termcolor",
]

[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
 "libc",
 "windows-sys 0.52.0",
]

[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"

[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
 "typenum",
 "version_check",
]

[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
 "unicode-width",
]

[[package]]
name = "getrandom"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
 "cfg-if",
 "libc",
 "wasi",
]

[[package]]
name = "globset"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
dependencies = [
 "aho-corasick",
 "bstr",
 "log",
 "regex-automata",
 "regex-syntax",
]

[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
 "bitflags 1.3.2",
 "ignore",
 "walkdir",
]

[[package]]
name = "handlebars"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225"
dependencies = [
 "log",
 "pest",
 "pest_derive",
 "serde",
 "serde_json",
 "thiserror",
]

[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"

[[package]]
name = "hermit-abi"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"

[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
 "libm",
]

[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"

[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
 "android_system_properties",
 "core-foundation-sys",
 "iana-time-zone-haiku",
 "js-sys",
 "wasm-bindgen",
 "windows-core",
]

[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
 "cc",
]

[[package]]
name = "ignore"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
dependencies = [
 "crossbeam-deque",
 "globset",
 "log",
 "memchr",
 "regex-automata",
 "same-file",
 "walkdir",
 "winapi-util",
]

[[package]]
name = "is-terminal"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
 "hermit-abi",
 "rustix",
 "windows-sys 0.48.0",
]

[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"

[[package]]
name = "js-sys"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
dependencies = [
 "wasm-bindgen",
]

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "libc"
version = "0.2.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"

[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"

[[package]]
name = "linux-raw-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"

[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"

[[package]]
name = "mdbook"
version = "0.4.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80992cb0e05f22cc052c99f8e883f1593b891014b96a8b4637fd274d7030c85e"
dependencies = [
 "anyhow",
 "chrono",
 "clap",
 "clap_complete",
 "env_logger",
 "handlebars",
 "log",
 "memchr",
 "once_cell",
 "opener",
 "pathdiff",
 "pulldown-cmark",
 "regex",
 "serde",
 "serde_json",
 "shlex",
 "tempfile",
 "toml",
 "topological-sort",
]

[[package]]
name = "mdbook-files"
version = "0.2.0"
dependencies = [
 "anyhow",
 "camino",
 "clap",
 "env_logger",
 "ignore",
 "log",
 "mdbook",
 "pulldown-cmark",
 "pulldown-cmark-to-cmark",
 "serde",
 "serde_json",
 "tera",
 "toml",
 "uuid",
]

[[package]]
name = "memchr"
version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"

[[package]]
name = "normpath"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5"
dependencies = [
 "windows-sys 0.48.0",
]

[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
 "autocfg",
]

[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"

[[package]]
name = "opener"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c62dcb6174f9cb326eac248f07e955d5d559c272730b6c03e396b443b562788"
dependencies = [
 "bstr",
 "normpath",
 "winapi",
]

[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
 "regex",
]

[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"

[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"

[[package]]
name = "pest"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5"
dependencies = [
 "memchr",
 "thiserror",
 "ucd-trie",
]

[[package]]
name = "pest_derive"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2"
dependencies = [
 "pest",
 "pest_generator",
]

[[package]]
name = "pest_generator"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227"
dependencies = [
 "pest",
 "pest_meta",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "pest_meta"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6"
dependencies = [
 "once_cell",
 "pest",
 "sha2",
]

[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
 "phf_shared",
]

[[package]]
name = "phf_codegen"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
 "phf_generator",
 "phf_shared",
]

[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
 "phf_shared",
 "rand",
]

[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
 "siphasher",
]

[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"

[[package]]
name = "proc-macro2"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
dependencies = [
 "unicode-ident",
]

[[package]]
name = "pulldown-cmark"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
dependencies = [
 "bitflags 1.3.2",
 "getopts",
 "memchr",
 "unicase",
]

[[package]]
name = "pulldown-cmark-to-cmark"
version = "11.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a883e495f8fc4f209521b03a89dde6f6f211ed7cf92e85693c82508e8b722710"
dependencies = [
 "pulldown-cmark",
]

[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
 "proc-macro2",
]

[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
 "libc",
 "rand_chacha",
 "rand_core",
]

[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
 "ppv-lite86",
 "rand_core",
]

[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
 "getrandom",
]

[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
 "bitflags 1.3.2",
]

[[package]]
name = "regex"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
dependencies = [
 "aho-corasick",
 "memchr",
 "regex-automata",
 "regex-syntax",
]

[[package]]
name = "regex-automata"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
 "aho-corasick",
 "memchr",
 "regex-syntax",
]

[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"

[[package]]
name = "rustix"
version = "0.38.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
dependencies = [
 "bitflags 2.4.1",
 "errno",
 "libc",
 "linux-raw-sys",
 "windows-sys 0.52.0",
]

[[package]]
name = "ryu"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"

[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
 "winapi-util",
]

[[package]]
name = "serde"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
 "serde_derive",
]

[[package]]
name = "serde_derive"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "serde_json"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
 "cfg-if",
 "cpufeatures",
 "digest",
]

[[package]]
name = "shlex"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"

[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"

[[package]]
name = "slug"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4"
dependencies = [
 "deunicode",
 "wasm-bindgen",
]

[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"

[[package]]
name = "syn"
version = "2.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "tempfile"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
 "cfg-if",
 "fastrand",
 "redox_syscall",
 "rustix",
 "windows-sys 0.48.0",
]

[[package]]
name = "tera"
version = "1.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
dependencies = [
 "chrono",
 "chrono-tz",
 "globwalk",
 "humansize",
 "lazy_static",
 "percent-encoding",
 "pest",
 "pest_derive",
 "rand",
 "regex",
 "serde",
 "serde_json",
 "slug",
 "unic-segment",
]

[[package]]
name = "termcolor"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449"
dependencies = [
 "winapi-util",
]

[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
 "rustix",
 "windows-sys 0.48.0",
]

[[package]]
name = "thiserror"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7"
dependencies = [
 "thiserror-impl",
]

[[package]]
name = "thiserror-impl"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
 "serde",
]

[[package]]
name = "topological-sort"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"

[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"

[[package]]
name = "ucd-trie"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"

[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
 "unic-char-range",
]

[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"

[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"

[[package]]
name = "unic-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
dependencies = [
 "unic-ucd-segment",
]

[[package]]
name = "unic-ucd-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
dependencies = [
 "unic-char-property",
 "unic-char-range",
 "unic-ucd-version",
]

[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
 "unic-common",
]

[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
 "version_check",
]

[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"

[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"

[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"

[[package]]
name = "uuid"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
 "getrandom",
 "serde",
]

[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
 "same-file",
 "winapi-util",
]

[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

[[package]]
name = "wasm-bindgen"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
 "cfg-if",
 "wasm-bindgen-macro",
]

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
 "bumpalo",
 "log",
 "once_cell",
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
 "quote",
 "wasm-bindgen-macro-support",
]

[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "wasm-bindgen-backend",
 "wasm-bindgen-shared",
]

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
 "winapi-i686-pc-windows-gnu",
 "winapi-x86_64-pc-windows-gnu",
]

[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
 "winapi",
]

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
 "windows-targets 0.48.5",
]

[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
 "windows-targets 0.48.5",
]

[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
 "windows-targets 0.52.0",
]

[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
 "windows_aarch64_gnullvm 0.48.5",
 "windows_aarch64_msvc 0.48.5",
 "windows_i686_gnu 0.48.5",
 "windows_i686_msvc 0.48.5",
 "windows_x86_64_gnu 0.48.5",
 "windows_x86_64_gnullvm 0.48.5",
 "windows_x86_64_msvc 0.48.5",
]

[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
 "windows_aarch64_gnullvm 0.52.0",
 "windows_aarch64_msvc 0.52.0",
 "windows_i686_gnu 0.52.0",
 "windows_i686_msvc 0.52.0",
 "windows_x86_64_gnu 0.52.0",
 "windows_x86_64_gnullvm 0.52.0",
 "windows_x86_64_msvc 0.52.0",
]

[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"

[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"

[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"

[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"

[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"

[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"

[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"

[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"

[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"

[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"

[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"

[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"

[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[package]
name = "mdbook-files"
version = "0.2.0"
edition = "2021"
authors = ["Patrick Elsen <pelsen@xfbs.net>"]
license = "MIT"
description = "Preprocessor for mdbook which renders files from a directory as an interactive widget"
repository = "https://github.com/xfbs/mdbook-files"
readme = "README.md"

[dependencies]
anyhow = "1.0.75"
camino = { version = "1.1.6", features = ["serde", "serde1"] }
clap = { version = "4.4.8", features = ["derive"] }
env_logger = "0.10.1"
ignore = "0.4.21"
log = "0.4.20"
mdbook = { version = "0.4.35", default-features = false }
pulldown-cmark = "0.9.3"
pulldown-cmark-to-cmark = "11.0.1"
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
tera = { version = "1.19.1", default-features = false, features = ["builtins"] }
toml = "0.5.11"
uuid = { version = "1.6.1", features = ["v4", "serde"] }
# mdbook-files

[![docs.rs status](https://img.shields.io/docsrs/mdbook-files)](https://docs.rs/mdbook-files)
[![Crates.io version](https://img.shields.io/crates/v/mdbook-files)](https://crates.io/crates/mdbook-files)

Preprocessor for [mdBook][mdBook] which renders files from a directory as an
interactive widget, with syntax highlighting.

![Example of mdbook-files](docs/src/example.png)

## Example

You can run the example by launching `mdbook` in the example directory in this
repository.

```
mdbook serve
```

## Usage

Install `mdbook-files` using `cargo`:

```
cargo install mdbook-files
```

Put the following into your `book.toml`:

```toml
[preprocessor.files]
prefix = "examples"
```

The prefix is a path, relative to which files are to be included.  It is
mandatory to give a prefix. Every include path in the book must be within this
prefix.

You will also need to add the `style.css` from this repository to your list of
extra CSS files:

```toml
[output.html]
additional-css = ["style.css"]
```

To use it, add something like this to your book:

~~~markdown
```files
title = "Files in subfolder"
paths = ["subfolder/**"]
```
~~~

This will produce a widget with all files in `examples/subfolder`, with the given
title. The content of this is a TOML document which contains configuration.

## License

MIT.

[mdBook]: https://github.com/rust-lang/mdBook/
use anyhow::{bail, Context as _, Result};
use camino::Utf8PathBuf;
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use log::*;
use mdbook::{
    book::{Book, Chapter},
    errors::Result as MdbookResult,
    preprocess::{Preprocessor, PreprocessorContext},
    BookItem,
};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use pulldown_cmark_to_cmark::cmark;
use serde::Deserialize;
use std::{collections::BTreeMap, fmt::Write};
use tera::Tera;
use toml::value::Value;
use uuid::Uuid;

/// Configuration for an invocation of files
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Files {
    /// Path to files
    pub path: Utf8PathBuf,

    /// Add a glob to the set of overrides.
    ///
    /// Globs provided here have precisely the same semantics as a single line in a gitignore file,
    /// where the meaning of `!` is inverted: namely, `!` at the beginning of a glob will ignore a
    /// file. Without `!`, all matches of the glob provided are treated as whitelist matches.
    #[serde(default)]
    pub files: Vec<String>,

    /// When specified, path to the file that is opened by default.
    #[serde(default)]
    pub default_file: Option<Utf8PathBuf>,

    /// Process ignores case insensitively
    #[serde(default)]
    pub ignore_case_insensitive: bool,

    /// Do not cross file system boundaries.
    ///
    /// When this option is enabled, directory traversal will not descend into directories that are
    /// on a different file system from the root path.
    #[serde(default)]
    pub same_file_system: bool,

    /// Select the file type given by name.
    #[serde(default)]
    pub types: Vec<String>,

    /// Enables ignoring hidden files.
    #[serde(default)]
    pub hidden: bool,

    /// Whether to follow symbolic links or not.
    #[serde(default)]
    pub follow_links: bool,

    /// Enables reading `.ignore` files.
    ///
    /// `.ignore` files have the same semantics as gitignore files and are supported by search
    /// tools such as ripgrep and The Silver Searcher.
    #[serde(default)]
    pub dot_ignore: bool,

    /// Enables reading a global `gitignore` file, whose path is specified in git’s `core.excludesFile`
    /// config option.
    #[serde(default)]
    pub git_global: bool,

    /// Enables reading `.git/info/exclude` files.
    #[serde(default)]
    pub git_exclude: bool,

    /// Enables reading `.gitignore` files.
    #[serde(default)]
    pub git_ignore: bool,

    /// Whether a git repository is required to apply git-related ignore rules (global rules,
    /// .gitignore and local exclude rules).
    #[serde(default)]
    pub require_git: bool,

    /// Enables reading ignore files from parent directories.
    #[serde(default)]
    pub git_ignore_parents: bool,

    /// The maximum depth to recurse.
    #[serde(default)]
    pub max_depth: Option<usize>,

    /// Whether to ignore files above the specified limit.
    #[serde(default)]
    pub max_filesize: Option<u64>,

    #[serde(default)]
    pub height: Option<String>,
}

/// Configuration for the plugin
#[derive(Deserialize)]
pub struct Config {
    pub prefix: Utf8PathBuf,
}

#[derive(Clone, Debug, Copy)]
pub struct Context<'a> {
    prefix: &'a Utf8PathBuf,
    tera: &'a Tera,
}

pub struct Instance<'a> {
    context: Context<'a>,
    data: Files,
    uuid: Uuid,
}

#[derive(Clone, Debug)]
pub enum TreeNode {
    Directory(BTreeMap<String, TreeNode>),
    File(Uuid),
}

impl Default for TreeNode {
    fn default() -> Self {
        TreeNode::Directory(Default::default())
    }
}

impl TreeNode {
    fn insert(&mut self, path: &[&str], uuid: Uuid) {
        match self {
            TreeNode::Directory(files) if path.len() == 1 => {
                files.insert(path[0].into(), TreeNode::File(uuid));
            }
            TreeNode::Directory(files) => {
                files
                    .entry(path[0].into())
                    .or_default()
                    .insert(&path[1..], uuid);
            }
            TreeNode::File(_file) => panic!("entry exists"),
        }
    }

    pub fn render(&self) -> Result<String> {
        let mut output = String::new();
        match self {
            TreeNode::File(_) => bail!("root node cannot be file"),
            TreeNode::Directory(files) => Self::render_files(&mut output, files)?,
        }
        Ok(output)
    }

    fn render_files(output: &mut dyn Write, files: &BTreeMap<String, TreeNode>) -> Result<()> {
        write!(output, "<ul>")?;
        for (path, node) in files {
            node.render_inner(output, path)?;
        }
        write!(output, "</ul>")?;
        Ok(())
    }

    fn render_inner(&self, output: &mut dyn Write, name: &str) -> Result<()> {
        match self {
            TreeNode::File(uuid) => {
                write!(
                    output,
                    r#"<li id="button-{uuid}" class="mdbook-files-button">{name}</li>"#
                )?;
            }
            TreeNode::Directory(files) => {
                write!(
                    output,
                    r#"<li class="mdbook-files-folder"><span>{name}/</span>"#
                )?;
                Self::render_files(output, files)?;
                write!(output, "</li>")?;
            }
        }
        Ok(())
    }
}

pub type FilesMap = BTreeMap<Utf8PathBuf, Uuid>;

impl<'a> Instance<'a> {
    fn parent(&self) -> Utf8PathBuf {
        self.context.prefix.join(&self.data.path)
    }

    fn files(&self) -> Result<FilesMap> {
        let mut paths: FilesMap = Default::default();
        let parent = self.parent();
        let mut overrides = OverrideBuilder::new(&parent);
        for item in &self.data.files {
            overrides.add(item)?;
        }
        let overrides = overrides.build()?;
        let mut walker = WalkBuilder::new(&parent);
        walker
            .standard_filters(false)
            .ignore_case_insensitive(self.data.ignore_case_insensitive)
            .same_file_system(self.data.same_file_system)
            .require_git(self.data.require_git)
            .hidden(self.data.hidden)
            .ignore(self.data.dot_ignore)
            .git_ignore(self.data.git_ignore)
            .git_exclude(self.data.git_exclude)
            .git_global(self.data.git_global)
            .parents(self.data.git_ignore_parents)
            .follow_links(self.data.follow_links)
            .max_depth(self.data.max_depth)
            .overrides(overrides)
            .max_filesize(self.data.max_filesize);

        let walker = walker.build();

        for path in walker {
            let path = path?;
            if path.file_type().unwrap().is_file() {
                paths.insert(path.path().to_path_buf().try_into()?, Uuid::new_v4());
            }
        }

        info!("Found {} matching files", paths.len());
        if paths.is_empty() {
            bail!("No files matched");
        }

        Ok(paths)
    }

    fn left(&self, files: &FilesMap) -> Result<String> {
        let mut output = String::new();
        let parent = self.parent();
        output.push_str(r#"<div class="mdbook-files-left">"#);

        let mut root = TreeNode::default();
        for (path, uuid) in files.iter() {
            let path = path.strip_prefix(&parent)?;
            let path: Vec<_> = path.components().map(|c| c.as_str()).collect();
            root.insert(&path[..], *uuid);
        }

        let list = root.render()?;
        output.push_str(&list);
        output.push_str("</div>");
        Ok(output)
    }

    fn right(&self, files: &FilesMap) -> Result<Vec<Event<'static>>> {
        let mut events = vec![];
        events.push(Event::Html(CowStr::Boxed(
            r#"<div class="mdbook-files-right">"#.to_string().into(),
        )));

        for (path, uuid) in files {
            info!("Reading {path}");
            let contents = std::fs::read_to_string(path)?;
            let extension = path.extension().unwrap_or("");
            let tag = Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Boxed(extension.into())));

            events.push(Event::Html(CowStr::Boxed(
                format!(r#"<div id="file-{uuid}" class="mdbook-file visible">"#).into(),
            )));

            events.push(Event::Start(tag.clone()));
            events.push(Event::Text(CowStr::Boxed(contents.into())));
            events.push(Event::End(tag));

            events.push(Event::Html(CowStr::Boxed("</div>".to_string().into())));
        }

        events.push(Event::Html(CowStr::Boxed("</div>".to_string().into())));
        Ok(events)
    }

    fn events(&self) -> Result<Vec<Event<'static>>> {
        let paths = self.files()?;

        let mut events = vec![];

        let height = self.data.height.as_deref().unwrap_or("300px");
        events.push(Event::Html(CowStr::Boxed(
            format!(
                r#"<div id="files-{}" class="mdbook-files" style="height: {height};">"#,
                self.uuid
            )
            .into(),
        )));

        events.push(Event::Html(CowStr::Boxed(self.left(&paths)?.into())));
        events.append(&mut self.right(&paths)?);
        events.push(Event::Html(CowStr::Boxed("</div>".to_string().into())));

        let uuids: Vec<Uuid> = paths.values().copied().collect();
        let visible = match &self.data.default_file {
            Some(file) => paths.get(&self.parent().join(file)).unwrap(),
            None => &uuids[0],
        };

        let mut context = tera::Context::new();
        context.insert("uuids", &uuids);
        context.insert("visible", visible);

        let script = self.context.tera.render("script", &context)?;

        events.push(Event::Html(CowStr::Boxed(
            format!("<script>{script}</script>").into(),
        )));

        events.push(Event::HardBreak);
        Ok(events)
    }
}

impl<'b> Context<'b> {
    fn map(&self, book: Book) -> Result<Book> {
        let mut book = book;
        book.sections = std::mem::take(&mut book.sections)
            .into_iter()
            .map(|section| self.map_book_item(section))
            .collect::<Result<_, _>>()?;
        Ok(book)
    }

    fn map_book_item(&self, item: BookItem) -> Result<BookItem> {
        let result = match item {
            BookItem::Chapter(chapter) => BookItem::Chapter(self.map_chapter(chapter)?),
            other => other,
        };

        Ok(result)
    }

    fn map_code(&self, code: CowStr<'_>) -> Result<Vec<Event<'static>>> {
        Instance {
            data: toml::from_str(&code)?,
            uuid: Uuid::new_v4(),
            context: *self,
        }
        .events()
    }

    fn label(&self) -> &str {
        "files"
    }

    fn map_chapter(&self, mut chapter: Chapter) -> Result<Chapter> {
        chapter.content = self.map_markdown(&chapter.content)?;
        chapter.sub_items = std::mem::take(&mut chapter.sub_items)
            .into_iter()
            .map(|item| self.map_book_item(item))
            .collect::<Result<_, _>>()?;
        Ok(chapter)
    }

    fn map_markdown(&self, markdown: &str) -> Result<String> {
        let mut parser = Parser::new_ext(markdown, Options::all());
        let mut events = vec![];

        loop {
            let next = parser.next();
            match next {
                None => break,
                Some(Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(label))))
                    if &*label == self.label() =>
                {
                    let mapped = match parser.next() {
                        Some(Event::Text(code)) => self.map_code(code).context("Mapping code")?,
                        other => unreachable!("Got {other:?}"),
                    };

                    for event in mapped.into_iter() {
                        events.push(event);
                    }

                    parser.next();
                }
                Some(event) => events.push(event),
            }
        }

        let mut buf = String::with_capacity(markdown.len());
        let output = cmark(events.iter(), &mut buf).map(|_| buf)?;
        Ok(output)
    }
}

#[derive(Clone, Debug)]
pub struct FilesPreprocessor {
    templates: Tera,
}

impl Default for FilesPreprocessor {
    fn default() -> Self {
        Self::new()
    }
}

impl FilesPreprocessor {
    pub fn new() -> Self {
        let mut templates = Tera::default();
        templates
            .add_raw_template("script", include_str!("script.js.tera"))
            .unwrap();
        Self { templates }
    }
}

impl Preprocessor for FilesPreprocessor {
    fn name(&self) -> &str {
        "files"
    }

    fn run(&self, ctx: &PreprocessorContext, book: Book) -> MdbookResult<Book> {
        let config = ctx.config.get_preprocessor(self.name()).unwrap();
        let config: Config = Value::Table(config.clone()).try_into().unwrap();
        let instance = Context {
            prefix: &config.prefix,
            tera: &self.templates,
        };
        instance.map(book)
    }
}
use anyhow::{bail, Result};
use clap::Parser;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook_files::FilesPreprocessor;
use options::{Command, Options};
use std::io;

mod options;

impl Options {
    fn run(&self, preprocessor: &dyn Preprocessor) -> Result<()> {
        match &self.command {
            Some(Command::Supports(command)) => {
                if preprocessor.supports_renderer(&command.renderer) {
                    Ok(())
                } else {
                    bail!("unknown renderer {}", command.renderer);
                }
            }
            None | Some(Command::Process) => {
                let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
                let output = preprocessor.run(&ctx, book)?;
                serde_json::to_writer(io::stdout(), &output)?;
                Ok(())
            }
            Some(Command::Install(_command)) => Ok(()),
        }
    }
}

fn main() -> Result<()> {
    env_logger::init();
    let options = options::Options::parse();
    let renderer = FilesPreprocessor::new();
    options.run(&renderer)
}
use clap::Parser;
use std::path::PathBuf;

/// Preprocessor for mdBook which renders files from a directory as an interactive widget, with
/// syntax highlighting.
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct Options {
    #[clap(subcommand)]
    pub command: Option<Command>,
}

#[derive(Parser, Debug)]
pub enum Command {
    /// Check if the renderer is supported.
    Supports(SupportsCommand),
    /// Process a parsed book (default).
    Process,
    /// Install support for mdbook-files into the current mdbook project.
    Install(InstallCommand),
}

#[derive(Parser, Debug)]
pub struct SupportsCommand {
    pub renderer: String,
}

#[derive(Parser, Debug)]
pub struct InstallCommand {
    #[clap(long)]
    pub assets: Option<PathBuf>,
}
window.addEventListener("load", (event) => {
    const uuids = {{ uuids | json_encode() }};
    function set_visible(uuid) {
        uuids.forEach((uuid) => {
            document.getElementById(`button-${uuid}`).classList.remove("active");
            document.getElementById(`file-${uuid}`).classList.remove("visible");
        });
        const button = document.getElementById(`button-${uuid}`).classList.add("active");
        const file = document.getElementById(`file-${uuid}`).classList.add("visible");
    }
    function add_hook(uuid) {
        const button = document.getElementById(`button-${uuid}`);
        button.addEventListener("click", (event) => set_visible(uuid));
    }
    uuids.forEach((uuid) => add_hook(uuid));
    set_visible({{ visible | json_encode() }});
});

/* style for mdbook-files */

.mdbook-files {
    display: flex;
    background-color: var(--sidebar-bg);
    margin: 1em 0;
}

.mdbook-files ul {
    list-style-type: none;
    padding: 2mm;
    margin: 0;
}

.mdbook-files li > ul {
    padding-top: 0;
    padding-bottom: 0;
}

.mdbook-files .mdbook-files-folder > span {
    font-weight: 600;
}

.mdbook-files ul .active {
    font-weight: 600;
}

.mdbook-files ul > li.mdbook-files-button {
    cursor: pointer;
}

.mdbook-files ul > li.mdbook-files-button:hover {
    color: var(--links);
}

.mdbook-files-left {
    width: 150px;
    min-width: 150px;
    overflow: scroll;
}

.mdbook-files-right {
    flex-grow: 1;
    overflow: scroll;
}

.mdbook-file {
    display: none;
}

.mdbook-file pre {
    margin: 0;
    height: 100%;
}

.mdbook-file pre code {
    height: 100%;
}

.mdbook-file.visible {
    display: block;
    height: 100%;
}

This is some paragraph after the thing.

Subchapter

This is a subchapter.

  • book.toml
  • src/
    • SUMMARY.md
    • examples.md
    • getting-started/
      • install.md
      • setup.md
      • usage.md
    • getting-started.md
    • reference/
      • files.md
      • plugin.md
    • reference.md
    • tests/
      • book.md
      • plugin.md
    • tests.md
[book]
authors = ["Patrick Elsen"]
language = "en"
multilingual = false
src = "src"
title = "mdbook-files book"

[preprocessor.files]
command = "cargo run --"
prefix = ".."

[output.html]
additional-css = ["style.css"]
# Summary

- [Getting Started](getting-started.md)
    - [Install](getting-started/install.md)
    - [Setup](getting-started/setup.md)
    - [Usage](getting-started/usage.md)
- [Reference](reference.md)
    - [Plugin](reference/plugin.md)
    - [Files](reference/files.md)
- [Examples](examples.md)
- [Tests](tests.md)
    - [Plugin Source](tests/plugin.md)
    - [Book Source](tests/book.md)
# Examples
# Install

To install `mdbook-files`, you have two options. The easiest installation path is
to download a prebuilt binary from the GitHub releases. If you have Cargo installed,
you can also build it from source.

### Prebuilt

You can go to the [Releases](https://github.com/xfbs/mdbook-files/releases) page and download
whichever prebuilt binary is appropriate for the operating system that you are using.

For example, to install it in Linux, you can do like this. You will need to set
the `MDBOOK_FILES_VERSION` to the latest version (or whichever version you want to
install).

```bash
export MDBOOK_FILES_VERSION=0.1.0
curl -sSL "https://github.com/xfbs/mdbook-files/releases/download/v$MDBOOK_FILES_VERSION/mdbook-files-v$MDBOOK_FILES_VERSION-x86_64-unknown-linux-musl.tar.gz" | sudo tar -C /usr/local/bin -xzv
```

### From Source

Another approach is to install `mdbook-files` from source. This approach works on operating
systems for which no builds exist. To do so, you need to have Cargo[^1] installed.

    cargo install mdbook-files --version 0.1.0

## Verify

Once you have installed it, you should be able to verify that you have installed it correctly
by running this command:

    mdbook-files --version

Which should respond with whichever version you have installed.

[^1]: Use [rustup](https://rustup.rs/) to install it, if neccessary.
# Setup

To get started with `mdbook-files`, the next step is to add it to your `mdbook`
configuration.  To do this, you have two options: the automatic install method,
and the manual installation method. The former method is recommended, but the
latter is still documented here in case you run into issues.

### Automatic setup

To install `mdbook-files` automatically, given that you have already installed it,
you can run this command:

    mdbook-files install

This will run perform the same steps as the manual installation method.

### Manual setup

Next, setup your project by adding this to the configuration:

```toml
[preprocessor.files]
prefix = "."

[output.html]
additional-css = ["style.css"]
```

Next, you need to add the `style.css` file into your project, by copying it
from the repository.

## Verify

Once you have done this, you should be able to run `mdbook` to build your book
and not get any issues.

    mdbook build

If this succeeds, you are ready for the next step.
# Usage

To use `mdbook-files`, simply drop something like this into your project's
markdown source:

~~~markdown
```files
path = "path/to/folder"
```
~~~

This will create a widget which renders all of the files in `path/to/files`.
# Getting Started

Preprocessor for [mdBook][mdBook] which renders files from a directory as an
interactive widget, with syntax highlighting.

![Example of mdbook-files](example.png)

I wrote this to make it easy to showcase examples inside books written using
mdBook, which have more than just one file, such as entire Rust crates. The
file picker makes it easy to browse around, and the syntax highlighting works
just as simple code examples would work.

[mdBook]: https://github.com/rust-lang/mdBook
# Files

To render files, a pseudo-code block needs to be added to your markdown source
that looks like this:

~~~markdown
```files
path = "path/to/folder"
# other config
```
~~~

The `mkdbook-files` plugin will pick up on these and replace them with file widgets.

This section explains the options available for every files instance.

```toml
# path to folder to select files to show
path = "path/to/folder"

# Override list for files. Files added here are included even if they are ignored,
# prefixing entries with an exclamation mark turns them into ignores.
files = ["*.png", "!*.md"]

# When set, is the default file to show.
default_file = "README.md"

# Process ignores case insensitively
ignore_case_insensitive = false

# Do not cross file system boundaries.
#
# When this option is enabled, directory traversal will not descend into directories that are
# on a different file system from the root path.
same_file_system = false

# Select the file type given by name.
types = ["png", "rust"]

# Enables ignoring hidden files.
hidden = false

# Whether to follow symbolic links or not.
follow_links = false

# Enables reading `.ignore` files.
#
# `.ignore` files have the same semantics as gitignore files and are supported by search
# tools such as ripgrep and The Silver Searcher.
dot_ignore = false

# Enables reading a global `gitignore` file, whose path is specified in git’s `core.excludesFile`
# config option.
git_global = false

# Enables reading `.git/info/exclude` files.
git_exclude = false

# Enables reading `.gitignore` files.
git_ignore = false

# Whether a git repository is required to apply git-related ignore rules (global rules,
# .gitignore and local exclude rules).
require_git = false

# Enables reading ignore files from parent directories.
git_ignore_parents = false

# Maximum depth to recurse.
#max_depth = 1234

# Ignore files above the specified limit.
#max_filesize = 10000
```
# Plugin

The plugin configuration is the settings which are in the `book.toml` file.

These are all of the options, along with their semantics and default values:

```toml
[preprocessor.files]

# specify name of or path to the binary
command = "mdbook-files"

# path prefix added to all invocations
prefix = "."
```
# Reference

This section explains all of the options for configurting `mdbook-files` and
all of the options for the instantiation.
# Subchapter

This is a subchapter.

```files
path = "docs"
files = ["!**/*.png"]
hidden = true
git_ignore = true
default_file = "src/SUMMARY.md"
```

## Subchapter
# Plugin Source

Source code of `mdbook-files`:

```files
path = "."
files = ["!.github", "!*.png", "!docs"]
hidden = true
git_ignore = true
```

This is some paragraph after the thing.
# Source

Source code of `mdbook-files`:

```files
path = "."
files = ["!.github", "!*.png", "!docs"]
hidden = true
git_ignore = true
```

This is some paragraph after the thing.

Subchapter