Automatically add exercise links to sections. (#52)
authorLuca Palmieri <20745048+LukeMathWalker@users.noreply.github.com>
Fri, 24 May 2024 16:15:38 +0000 (18:15 +0200)
committerGitHub <noreply@github.com>
Fri, 24 May 2024 16:15:38 +0000 (18:15 +0200)
We use an mdbook preprocessor to automatically generate links to the relevant exercise for each section.
We remove all existing manual links and refactor the deploy process to push the rendered book to a branch.

65 files changed:
.github/workflows/ci.yml
Cargo.lock
Cargo.toml
book/book.toml
book/src/01_intro/00_welcome.md
book/src/01_intro/01_syntax.md
book/src/02_basic_calculator/00_intro.md
book/src/02_basic_calculator/01_integers.md
book/src/02_basic_calculator/02_variables.md
book/src/02_basic_calculator/03_if_else.md
book/src/02_basic_calculator/04_panics.md
book/src/02_basic_calculator/05_factorial.md
book/src/02_basic_calculator/06_while.md
book/src/02_basic_calculator/07_for.md
book/src/02_basic_calculator/08_overflow.md
book/src/02_basic_calculator/09_saturating.md
book/src/02_basic_calculator/10_as_casting.md
book/src/03_ticket_v1/00_intro.md
book/src/03_ticket_v1/01_struct.md
book/src/03_ticket_v1/02_validation.md
book/src/03_ticket_v1/03_modules.md
book/src/03_ticket_v1/04_visibility.md
book/src/03_ticket_v1/05_encapsulation.md
book/src/03_ticket_v1/06_ownership.md
book/src/03_ticket_v1/07_setters.md
book/src/03_ticket_v1/08_stack.md
book/src/03_ticket_v1/09_heap.md
book/src/03_ticket_v1/10_references_in_memory.md
book/src/03_ticket_v1/11_destructor.md
book/src/03_ticket_v1/12_outro.md
book/src/04_traits/00_intro.md
book/src/04_traits/01_trait.md
book/src/04_traits/02_orphan_rule.md
book/src/04_traits/03_operator_overloading.md
book/src/04_traits/04_derive.md
book/src/04_traits/05_trait_bounds.md
book/src/04_traits/06_str_slice.md
book/src/04_traits/07_deref.md
book/src/04_traits/08_sized.md
book/src/04_traits/09_from.md
book/src/04_traits/10_assoc_vs_generic.md
book/src/04_traits/11_clone.md
book/src/04_traits/12_copy.md
book/src/04_traits/13_drop.md
book/src/04_traits/14_outro.md
book/src/05_ticket_v2/00_intro.md
book/src/05_ticket_v2/01_enum.md
book/src/05_ticket_v2/02_match.md
book/src/05_ticket_v2/03_variants_with_data.md
book/src/05_ticket_v2/04_if_let.md
book/src/05_ticket_v2/05_nullability.md
book/src/05_ticket_v2/06_fallibility.md
book/src/05_ticket_v2/07_unwrap.md
book/src/05_ticket_v2/08_error_enums.md
book/src/05_ticket_v2/09_error_trait.md
book/src/05_ticket_v2/10_packages.md
book/src/05_ticket_v2/11_dependencies.md
book/src/05_ticket_v2/12_thiserror.md
book/src/05_ticket_v2/13_try_from.md
book/src/05_ticket_v2/14_source.md
book/src/05_ticket_v2/15_outro.md
book/src/06_ticket_management/06_lifetimes.md
helpers/mdbook-exercise-linker/Cargo.toml [new file with mode: 0644]
helpers/mdbook-exercise-linker/src/lib.rs [new file with mode: 0644]
helpers/mdbook-exercise-linker/src/main.rs [new file with mode: 0644]

index 6fbe7bf..af89223 100644 (file)
@@ -9,22 +9,20 @@ on:
       - main
 
 jobs:
-  formatter:
+  build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: dprint/check@v2.2
-
-  check-links:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+      - uses: actions-rust-lang/setup-rust-toolchain@v1
+      - name: Install plugin
+        run: cargo install --path helpers/mdbook-exercise-linker
+      - uses: taiki-e/install-action@v2
+        with:
+          tool: mdbook
       - name: Build book
         run: |
           cd book
-          curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.40/mdbook-v0.4.40-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=.
-          ./mdbook build
+          mdbook build
       - name: Link Checker
         uses: lycheeverse/lychee-action@v1
         with:
@@ -34,3 +32,25 @@ jobs:
             --require-https 
             --no-progress 
             book/book
+      # Upload the book as an artifact
+      - uses: actions/upload-artifact@v4
+        with:
+          name: book
+          path: book/book
+            # Commit and push all changed files.
+            # Must only affect files that are listed in "paths-ignore".
+      - name: Git commit build artifacts
+        # Only run on main branch push (e.g. pull request merge).
+        if: github.event_name == 'push'
+        run: |
+          git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}"
+          git config --global user.email "username@users.noreply.github.com"
+          git add --force book/book
+          git commit -m "${{ env.CI_COMMIT_MESSAGE }}"
+          git push --set-upstream --force-with-lease origin deploy
+
+  formatter:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: dprint/check@v2.2
\ No newline at end of file
index 5b93b94..916ec52 100644 (file)
@@ -17,11 +17,97 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ammonia"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ab99eae5ee58501ab236beb6f20f6ca39be615267b014899c89b2f0bc18a459"
+dependencies = [
+ "html5ever",
+ "maplit",
+ "once_cell",
+ "tendril",
+ "url",
+]
+
+[[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.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
 
 [[package]]
 name = "arrays"
@@ -71,12 +157,33 @@ dependencies = [
  "rustc-demangle",
 ]
 
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
 [[package]]
 name = "bitflags"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
 
+[[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 = "blocking"
 version = "0.1.0"
@@ -92,6 +199,17 @@ dependencies = [
  "ticket_fields",
 ]
 
+[[package]]
+name = "bstr"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
 [[package]]
 name = "btreemap"
 version = "0.1.0"
@@ -99,6 +217,18 @@ dependencies = [
  "ticket_fields",
 ]
 
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
 [[package]]
 name = "bytes"
 version = "1.6.0"
@@ -131,6 +261,55 @@ dependencies = [
  "ticket_fields",
 ]
 
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
+dependencies = [
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_complete"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e"
+dependencies = [
+ "clap",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
 [[package]]
 name = "client"
 version = "0.1.0"
@@ -142,6 +321,12 @@ dependencies = [
 name = "clone"
 version = "0.1.0"
 
+[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
 [[package]]
 name = "combinators"
 version = "0.1.0"
@@ -158,195 +343,754 @@ name = "copy"
 version = "0.1.0"
 
 [[package]]
-name = "deps"
-version = "0.1.0"
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
 [[package]]
-name = "deref"
-version = "0.1.0"
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
 
 [[package]]
-name = "derives"
-version = "0.1.0"
+name = "crossbeam-channel"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+dependencies = [
+ "crossbeam-utils",
+]
 
 [[package]]
-name = "destructor"
-version = "0.1.0"
+name = "crossbeam-deque"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
 
 [[package]]
-name = "drop"
-version = "0.1.0"
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
 
 [[package]]
-name = "encapsulation"
-version = "0.1.0"
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
 
 [[package]]
-name = "enum_"
-version = "0.1.0"
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
- "common",
+ "generic-array",
+ "typenum",
 ]
 
 [[package]]
-name = "error_enums"
-version = "0.1.0"
-dependencies = [
- "common",
-]
+name = "data-encoding"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
 
 [[package]]
-name = "error_trait"
-version = "0.1.0"
+name = "dbus"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
 dependencies = [
- "common",
- "static_assertions",
+ "libc",
+ "libdbus-sys",
+ "winapi",
 ]
 
 [[package]]
-name = "factorial"
+name = "deps"
 version = "0.1.0"
 
 [[package]]
-name = "fallibility"
+name = "deref"
 version = "0.1.0"
-dependencies = [
- "common",
-]
 
 [[package]]
-name = "for_"
+name = "derives"
 version = "0.1.0"
 
 [[package]]
-name = "from"
+name = "destructor"
 version = "0.1.0"
 
 [[package]]
-name = "future"
-version = "0.1.0"
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
- "tokio",
+ "block-buffer",
+ "crypto-common",
 ]
 
 [[package]]
-name = "gimli"
-version = "0.28.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+name = "drop"
+version = "0.1.0"
 
 [[package]]
-name = "hashmap"
-version = "0.1.0"
+name = "elasticlunr-rs"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41e83863a500656dfa214fee6682de9c5b9f03de6860fec531235ed2ae9f6571"
 dependencies = [
- "ticket_fields",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
 ]
 
 [[package]]
-name = "heap"
+name = "encapsulation"
 version = "0.1.0"
 
 [[package]]
-name = "hermit-abi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
-
-[[package]]
-name = "if_else"
+name = "enum_"
 version = "0.1.0"
+dependencies = [
+ "common",
+]
 
 [[package]]
-name = "if_let"
+name = "env_filter"
 version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
+dependencies = [
+ "log",
+ "regex",
+]
 
 [[package]]
-name = "impl_trait"
-version = "0.1.0"
+name = "env_logger"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
 dependencies = [
- "ticket_fields",
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
 ]
 
 [[package]]
-name = "impl_trait_2"
-version = "0.1.0"
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
 dependencies = [
- "ticket_fields",
+ "libc",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
-name = "index"
+name = "error_enums"
 version = "0.1.0"
 dependencies = [
- "ticket_fields",
+ "common",
 ]
 
 [[package]]
-name = "index_mut"
+name = "error_trait"
 version = "0.1.0"
 dependencies = [
- "ticket_fields",
+ "common",
+ "static_assertions",
 ]
 
 [[package]]
-name = "integers"
+name = "factorial"
 version = "0.1.0"
 
 [[package]]
-name = "interior_mutability"
+name = "fallibility"
 version = "0.1.0"
+dependencies = [
+ "common",
+]
 
 [[package]]
-name = "intro_01"
-version = "0.1.0"
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
 
 [[package]]
-name = "intro_02"
-version = "0.1.0"
+name = "filetime"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.4.1",
+ "windows-sys 0.52.0",
+]
 
 [[package]]
-name = "intro_03"
-version = "0.1.0"
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
-name = "intro_04"
+name = "for_"
 version = "0.1.0"
 
 [[package]]
-name = "intro_05"
-version = "0.1.0"
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
 
 [[package]]
-name = "intro_07"
+name = "from"
 version = "0.1.0"
 
 [[package]]
-name = "intro_08"
-version = "0.1.0"
+name = "fsevent-sys"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
+dependencies = [
+ "libc",
+]
 
 [[package]]
-name = "iter"
-version = "0.1.0"
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
 dependencies = [
- "ticket_fields",
+ "mac",
+ "new_debug_unreachable",
 ]
 
 [[package]]
-name = "iterators"
+name = "future"
 version = "0.1.0"
 dependencies = [
- "ticket_fields",
+ "tokio",
 ]
 
 [[package]]
-name = "leaking"
-version = "0.1.0"
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
 
 [[package]]
-name = "libc"
-version = "0.2.154"
+name = "futures-core"
+version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[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 = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
+
+[[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 = "h2"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "handlebars"
+version = "5.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
+dependencies = [
+ "log",
+ "pest",
+ "pest_derive",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashmap"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "headers"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
+dependencies = [
+ "base64",
+ "bytes",
+ "headers-core",
+ "http 0.2.12",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
+dependencies = [
+ "http 0.2.12",
+]
+
+[[package]]
+name = "heap"
+version = "0.1.0"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "html5ever"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "0.14.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+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 = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "if_else"
+version = "0.1.0"
+
+[[package]]
+name = "if_let"
+version = "0.1.0"
+
+[[package]]
+name = "ignore"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "impl_trait"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "impl_trait_2"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "index"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "index_mut"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "inotify"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
+dependencies = [
+ "bitflags 1.3.2",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "integers"
+version = "0.1.0"
+
+[[package]]
+name = "interior_mutability"
+version = "0.1.0"
+
+[[package]]
+name = "intro_01"
+version = "0.1.0"
+
+[[package]]
+name = "intro_02"
+version = "0.1.0"
+
+[[package]]
+name = "intro_03"
+version = "0.1.0"
+
+[[package]]
+name = "intro_04"
+version = "0.1.0"
+
+[[package]]
+name = "intro_05"
+version = "0.1.0"
+
+[[package]]
+name = "intro_07"
+version = "0.1.0"
+
+[[package]]
+name = "intro_08"
+version = "0.1.0"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
+name = "iter"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "iterators"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kqueue"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
+dependencies = [
+ "kqueue-sys",
+ "libc",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+]
+
+[[package]]
+name = "leaking"
+version = "0.1.0"
+
+[[package]]
+name = "libc"
+version = "0.2.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
 
 [[package]]
 name = "lifetime"
@@ -355,6 +1099,12 @@ dependencies = [
  "ticket_fields",
 ]
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
 [[package]]
 name = "lock_api"
 version = "0.4.12"
@@ -366,16 +1116,95 @@ dependencies = [
 ]
 
 [[package]]
-name = "locks"
+name = "locks"
+version = "0.1.0"
+dependencies = [
+ "thiserror",
+ "ticket_fields",
+]
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "markup5ever"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "match_"
 version = "0.1.0"
+
+[[package]]
+name = "mdbook"
+version = "0.4.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5"
 dependencies = [
- "thiserror",
- "ticket_fields",
+ "ammonia",
+ "anyhow",
+ "chrono",
+ "clap",
+ "clap_complete",
+ "elasticlunr-rs",
+ "env_logger",
+ "futures-util",
+ "handlebars",
+ "ignore",
+ "log",
+ "memchr",
+ "notify",
+ "notify-debouncer-mini",
+ "once_cell",
+ "opener",
+ "pathdiff",
+ "pulldown-cmark",
+ "regex",
+ "serde",
+ "serde_json",
+ "shlex",
+ "tempfile",
+ "tokio",
+ "toml",
+ "topological-sort",
+ "walkdir",
+ "warp",
 ]
 
 [[package]]
-name = "match_"
+name = "mdbook-exercise-linker"
 version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "mdbook",
+ "semver",
+ "serde_json",
+]
 
 [[package]]
 name = "memchr"
@@ -383,6 +1212,22 @@ version = "2.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
 
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
 [[package]]
 name = "miniz_oxide"
 version = "0.7.2"
@@ -399,6 +1244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 dependencies = [
  "libc",
+ "log",
  "wasi",
  "windows-sys 0.48.0",
 ]
@@ -411,6 +1257,51 @@ version = "0.1.0"
 name = "mut_slice"
 version = "0.1.0"
 
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "normpath"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5831952a9476f2fed74b77d74182fa5ddc4d21c72ec45a333b250e3ed0272804"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "notify"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
+dependencies = [
+ "bitflags 2.5.0",
+ "crossbeam-channel",
+ "filetime",
+ "fsevent-sys",
+ "inotify",
+ "kqueue",
+ "libc",
+ "log",
+ "mio",
+ "walkdir",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "notify-debouncer-mini"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
+dependencies = [
+ "crossbeam-channel",
+ "log",
+ "notify",
+]
+
 [[package]]
 name = "nullability"
 version = "0.1.0"
@@ -418,6 +1309,15 @@ dependencies = [
  "common",
 ]
 
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "num_cpus"
 version = "1.16.0"
@@ -437,6 +1337,24 @@ dependencies = [
  "memchr",
 ]
 
+[[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.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8df34be653210fbe9ffaff41d3b92721c56ce82dfee58ee684f9afb5e3a90c0"
+dependencies = [
+ "bstr",
+ "dbus",
+ "normpath",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "orphan"
 version = "0.1.0"
@@ -498,7 +1416,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.5.1",
  "smallvec",
  "windows-targets 0.52.5",
 ]
@@ -511,12 +1429,170 @@ dependencies = [
  "ticket_fields",
 ]
 
+[[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.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8"
+dependencies = [
+ "memchr",
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd"
+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 0.11.2",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
+dependencies = [
+ "phf_generator 0.11.2",
+ "phf_shared 0.11.2",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared 0.11.2",
+ "rand",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
 
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.82"
@@ -526,6 +1602,24 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "pulldown-cmark"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993"
+dependencies = [
+ "bitflags 2.5.0",
+ "memchr",
+ "pulldown-cmark-escape",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3"
+
 [[package]]
 name = "quote"
 version = "1.0.36"
@@ -535,73 +1629,252 @@ 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 = "redox_syscall"
 version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
 dependencies = [
- "bitflags",
+ "bitflags 2.5.0",
 ]
 
 [[package]]
 name = "references_in_memory"
 version = "0.1.0"
 
+[[package]]
+name = "regex"
+version = "1.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
+
 [[package]]
 name = "resizing"
 version = "0.1.0"
 
 [[package]]
-name = "response"
-version = "0.1.0"
+name = "response"
+version = "0.1.0"
+dependencies = [
+ "ticket_fields",
+]
+
+[[package]]
+name = "runtime"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "tokio",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags 2.5.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rwlock"
+version = "0.1.0"
+dependencies = [
+ "thiserror",
+ "ticket_fields",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[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 = "saturating"
+version = "0.1.0"
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scoped_threads"
+version = "0.1.0"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
+
+[[package]]
+name = "serde"
+version = "1.0.202"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
 dependencies = [
- "ticket_fields",
+ "serde_derive",
 ]
 
 [[package]]
-name = "runtime"
-version = "0.1.0"
+name = "serde_derive"
+version = "1.0.202"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
 dependencies = [
- "anyhow",
- "tokio",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
-name = "rustc-demangle"
-version = "0.1.24"
+name = "serde_json"
+version = "1.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
-
-[[package]]
-name = "rwlock"
-version = "0.1.0"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
 dependencies = [
- "thiserror",
- "ticket_fields",
+ "itoa",
+ "ryu",
+ "serde",
 ]
 
 [[package]]
-name = "saturating"
-version = "0.1.0"
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
 
 [[package]]
-name = "scoped_threads"
+name = "setters"
 version = "0.1.0"
+dependencies = [
+ "common",
+]
 
 [[package]]
-name = "scopeguard"
-version = "1.2.0"
+name = "sha1"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
 
 [[package]]
-name = "setters"
-version = "0.1.0"
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
 dependencies = [
- "common",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
 ]
 
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.2"
@@ -611,10 +1884,25 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "sized"
 version = "0.1.0"
 
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "slice"
 version = "0.1.0"
@@ -672,6 +1960,38 @@ dependencies = [
  "common",
 ]
 
+[[package]]
+name = "string_cache"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared 0.10.0",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
 [[package]]
 name = "struct_"
 version = "0.1.0"
@@ -695,6 +2015,39 @@ version = "0.1.0"
 name = "syntax"
 version = "0.1.0"
 
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[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.60"
@@ -734,6 +2087,21 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
 [[package]]
 name = "tokio"
 version = "1.37.0"
@@ -764,6 +2132,72 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tokio-tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[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 = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
 [[package]]
 name = "trait_"
 version = "0.1.0"
@@ -772,10 +2206,35 @@ version = "0.1.0"
 name = "trait_bounds"
 version = "0.1.0"
 
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
 [[package]]
 name = "tryfrom"
 version = "0.1.0"
 
+[[package]]
+name = "tungstenite"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http 1.1.0",
+ "httparse",
+ "log",
+ "rand",
+ "sha1",
+ "thiserror",
+ "url",
+ "utf-8",
+]
+
 [[package]]
 name = "two_states"
 version = "0.1.0"
@@ -783,12 +2242,48 @@ dependencies = [
  "ticket_fields",
 ]
 
+[[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 = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
 [[package]]
 name = "unwrap"
 version = "0.1.0"
@@ -796,6 +2291,29 @@ dependencies = [
  "common",
 ]
 
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
 [[package]]
 name = "validation"
 version = "0.1.0"
@@ -818,16 +2336,123 @@ dependencies = [
 name = "vec"
 version = "0.1.0"
 
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
 [[package]]
 name = "visibility"
 version = "0.1.0"
 
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "warp"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "headers",
+ "http 0.2.12",
+ "hyper",
+ "log",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-tungstenite",
+ "tokio-util",
+ "tower-service",
+ "tracing",
+]
+
 [[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.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
 [[package]]
 name = "welcome_00"
 version = "0.1.0"
@@ -836,6 +2461,46 @@ version = "0.1.0"
 name = "while_"
 version = "0.1.0"
 
+[[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.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.5",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
index 8267841..914c0ae 100644 (file)
@@ -1,3 +1,3 @@
 [workspace]
-members = ["exercises/*/*", "helpers/common", "helpers/ticket_fields"]
+members = ["exercises/*/*", "helpers/common", "helpers/mdbook-exercise-linker", "helpers/ticket_fields"]
 resolver = "2"
index b62716c..ea142b0 100644 (file)
@@ -7,3 +7,6 @@ title = "100 Exercises To Learn Rust"
 
 [output.html]
 git-repository-url = "https://github.com/mainmatter/100-exercises-to-learn-rust"
+
+[preprocessor.exercise-linker]
+exercise_root_url = "https://github.com/mainmatter/100-exercises-to-learn-rust/tree/main/exercises"
index 69a03a5..5ef4e24 100644 (file)
@@ -88,10 +88,6 @@ Don't move on to the next section until you've solved the exercise for the curre
 
 Enjoy the course!
 
-## References
-
-- The exercise for this section is located in `exercises/01_intro/00_welcome`
-
 ## Author
 
 This course was written by [Luca Palmieri](https://www.lpalmieri.com/), Principal Engineering
index 61e39f5..af5eed0 100644 (file)
@@ -114,6 +114,3 @@ tag, the compiler can enforce different rulesā€”e.g. you can't add a string to a
 together.
 If leveraged correctly, types can prevent whole classes of runtime bugs.
 
-## References
-
-- The exercise for this section is located in `exercises/01_intro/01_syntax`
index d62ec78..9a0d624 100644 (file)
@@ -15,6 +15,3 @@ Nailing the basics with a few exercises will get the language flowing under your
 When we move on to more complex topics, such as traits and ownership, you'll be able to focus on the new concepts
 without getting bogged down by the syntax or other trivial details.
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/00_intro`
index 5a597a3..765b7b4 100644 (file)
@@ -119,10 +119,6 @@ error[E0308]: mismatched types
 
 We'll see how to convert between types [later in this course](../04_traits/09_from.md).
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/01_integers`
-
 ## Further reading
 
 - [The integer types section](https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types) in the official Rust book
index b9628ff..2c5b21a 100644 (file)
@@ -97,8 +97,4 @@ help: consider assigning a value
   |            +++
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/02_variables`
-
 [^speed]: The Rust compiler needs all the help it can get when it comes to compilation speed.
index a6a1f13..2ba20b0 100644 (file)
@@ -100,6 +100,3 @@ In the example above, each branch of the `if` evaluates to a string literal,
 which is then assigned to the `message` variable.\
 The only requirement is that both `if` branches return the same type.
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/03_if_else`
index 96fc555..f7115aa 100644 (file)
@@ -41,10 +41,6 @@ fn main() {
 There are other mechanisms to work with recoverable errors in Rust, which [we'll cover later](../05_ticket_v2/06_fallibility.md).
 For the time being we'll stick with panics as a brutal but simple stopgap solution.
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/04_panics`
-
 ## Further reading
 
 - [The panic! macro documentation](https://doc.rust-lang.org/std/macro.panic.html)
index 2ace7fa..e0d0c76 100644 (file)
@@ -10,6 +10,3 @@ So far you've learned:
 
 It looks like you're ready to tackle factorials!
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/05_factorial`
index 69008fa..150d887 100644 (file)
@@ -80,10 +80,6 @@ while i <= 5 {
 
 This will compile and run without errors.
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/06_while`
-
 ## Further reading
 
 - [`while` loop documentation](https://doc.rust-lang.org/std/keyword.while.html)
index 8f3008b..896ffe4 100644 (file)
@@ -54,10 +54,6 @@ for i in 1..(end + 1) {
 }
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/07_for`
-
 ## Further reading
 
 - [`for` loop documentation](https://doc.rust-lang.org/std/keyword.for.html)
index 05e1b9a..aaaf8f5 100644 (file)
@@ -100,10 +100,6 @@ Our recommendation is to enable `overflow-checks` for both profiles: it's better
 incorrect results. The runtime performance hit is negligible in most cases; if you're working on a performance-critical
 application, you can run benchmarks to decide if it's something you can afford.
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/08_overflow`
-
 ## Further reading
 
 - Check out ["Myths and legends about integer overflow in Rust"](https://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/)
index 8f95c93..a33b6c2 100644 (file)
@@ -35,9 +35,5 @@ The opposite happens for underflows: `0 - 1` is `-1`, which is smaller than `u8:
 You can't get saturating arithmetic via the `overflow-checks` profile settingā€”you have to explicitly opt into it
 when performing the arithmetic operation.
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/09_saturating`
-
 [^method]: You can think of methods as functions that are "attached" to a specific type.
 We'll cover methods (and how to define them) in the next chapter.
index 51d73e5..0b8bedf 100644 (file)
@@ -94,10 +94,6 @@ When working with composite types, you'll have to rely on
 different conversion mechanisms ([fallible](../05_ticket_v2/13_try_from.md)
 and [infallible](../04_traits/09_from.md)), which we'll explore later on.
 
-## References
-
-- The exercise for this section is located in `exercises/02_basic_calculator/10_as_casting`
-
 ## Further reading
 
 - Check out [Rust's official reference](https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast)
index 8f5353b..903389a 100644 (file)
@@ -17,6 +17,3 @@ To move forward you'll have to pick up several new Rust concepts, such as:
 - Modules and visibility
 - Strings
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/00_intro`
index 76ae49f..a14d17e 100644 (file)
@@ -137,6 +137,3 @@ let is_open = Ticket::is_open(ticket);
 The function call syntax makes it quite clear that `ticket` is being used as `self`, the first parameter of the method,
 but it's definitely more verbose. Prefer the method call syntax when possible.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/01_struct`
index fa8e90d..2d8caf1 100644 (file)
@@ -15,10 +15,6 @@ This means that users can create a ticket with an empty title, a suuuuuuuper lon
 a nonsensical status (e.g. "Funny").\
 We can do better than that!
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/02_validation`
-
 ## Further reading
 
 - Check out [`String`'s documentation](https://doc.rust-lang.org/std/string/struct.String.html)
index be0dc18..7de10f0 100644 (file)
@@ -113,6 +113,3 @@ Nonetheless, it can be useful in some cases, like when writing unit tests. You m
 that most of our test modules start with a `use super::*;` statement to bring all the items from the parent module
 (the one being tested) into scope.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/03_modules`
index 12237a2..6338857 100644 (file)
@@ -44,6 +44,3 @@ pub struct Configuration {
 `Configuration` is public, but you can only access the `version` field from within the same crate.
 The `active` field, instead, is private and can only be accessed from within the same module or one of its submodules.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/04_visibility`
index da52cd4..17c249d 100644 (file)
@@ -56,8 +56,4 @@ Accessor methods are public methods that allow you to read the value of a privat
 Rust doesn't have a built-in way to generate accessor methods for you, like some other languages do.
 You have to write them yourselfā€”they're just regular methods.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/05_encapsulation`
-
 [^newtype]: Or refine their type, a technique we'll explore [later on](../05_ticket_v2/15_outro.md).
index 2676b2f..70aff26 100644 (file)
@@ -229,9 +229,5 @@ and truly understand how they work.
 Towards the end of this chapter we'll explain _why_ Rust's ownership system is designed the way it is.
 For the time being, focus on understanding the _how_. Take each compiler error as a learning opportunity!
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/06_ownership`
-
 [^refine]: This is a great mental model to start out, but it doesn't capture the _full_ picture.
 We'll refine our understanding of references [later in the course](../07_threads/06_interior_mutability.md).
index 59c6c39..7303122 100644 (file)
@@ -107,6 +107,3 @@ ticket.set_description("New description".into());
 ticket.set_status("In Progress".into());
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/07_setters`
index 3d44b6d..550e91b 100644 (file)
@@ -52,10 +52,6 @@ assert_eq!(std::mem::size_of::<u8>(), 1);
 
 1 makes sense, because a `u8` is 8 bits long, or 1 byte.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/08_stack`
-
 [^stack-overflow]: If you have nested function calls, each function pushes its data onto the stack when it's called but
 it doesn't pop it off until the innermost function returns.
 If you have too many nested function calls, you can run out of stack spaceā€”the stack is not infinite!
index aa17091..f32ef62 100644 (file)
@@ -133,10 +133,6 @@ but there is no general-purpose "API" to retrieve runtime heap usage in Rust.\
 You can, however, use a memory profiler tool (e.g. [DHAT](https://valgrind.org/docs/manual/dh-manual.html)
 or [a custom allocator](https://docs.rs/dhat/latest/dhat/)) to inspect the heap usage of your program.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/09_heap`
-
 [^empty]: `std` doesn't allocate if you create an **empty** `String` (i.e. `String::new()`).
 Heap memory will be reserved when you push data into it for the first time.
 
index 5821aec..bca87a1 100644 (file)
@@ -45,10 +45,6 @@ The same goes for `&mut String`.
 The example above should clarify one thing: not all pointers point to the heap.\
 They just point to a memory location, which _may_ be on the heap, but doesn't have to be.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/10_references_in_memory`
-
 [^fat]: [Later in the course](../04_traits/06_str_slice.md) we'll talk about **fat pointers**,
 i.e. pointers with additional metadata. As the name implies, they are larger than
 the pointers we discussed in this chapter, also known as **thin pointers**.
index 11217b5..aebc1bd 100644 (file)
@@ -165,9 +165,5 @@ They would refer to a memory location that's no longer valid: a so-called [**dan
 a close relative of [**use-after-free bugs**](https://owasp.org/www-community/vulnerabilities/Using_freed_memory).
 Rust's ownership system rules out these kinds of bugs by design.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/11_destructor`
-
 [^leak]: Rust doesn't guarantee that destructors will run. They won't, for example, if
 you explicitly choose to [leak memory](../07_threads/03_leak.md).
index 2d11fc1..8da9e57 100644 (file)
@@ -4,6 +4,3 @@ We've covered a lot of foundational Rust concepts in this chapter.\
 Before moving on, let's go through one last exercise to consolidate what we've learned.
 You'll have minimal guidance this timeā€”just the exercise description and the tests to guide you.
 
-## References
-
-- The exercise for this section is located in `exercises/03_ticket_v1/12_outro`
index fa47454..adead4e 100644 (file)
@@ -19,6 +19,3 @@ On top of traits as a concept, we'll also cover some of the key traits that are
 Since we'll be talking about conversions, we'll seize the opportunity to plug some of the "knowledge gaps"
 from the previous chapterā€”e.g. what is `"A title"`, exactly? Time to learn more about slices too!
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/00_intro`
index 38d4615..210eab4 100644 (file)
@@ -124,8 +124,4 @@ This is not necessary if:
 You can find the list of traits and types in the prelude in the
 [Rust documentation](https://doc.rust-lang.org/std/prelude/index.html).
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/01_trait`
-
 [^inherent]: A method defined directly on a type, without using a trait, is also known as an **inherent method**.
index d664efe..5bd47c4 100644 (file)
@@ -105,10 +105,6 @@ Which implementation should be used? The one defined in `B`? Or the one defined
 There's no good answer, therefore the orphan rule was defined to prevent this scenario.
 Thanks to the orphan rule, neither crate `B` nor crate `C` would compile.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/02_orphan_rule`
-
 ## Further reading
 
 - There are some caveats and exceptions to the orphan rule as stated above.
index 42a4721..0aba3a5 100644 (file)
@@ -97,6 +97,3 @@ impl PartialEq for MyType {
 }
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/03_operator_overloading`
index 4a078cc..f7e4198 100644 (file)
@@ -97,10 +97,6 @@ impl ::core::cmp::PartialEq for Ticket {
 
 The compiler will nudge you to derive traits when possible.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/04_derive`
-
 ## Further reading
 
 - [The little book of Rust macros](https://veykril.github.io/tlborm/)
index 15d8c93..a9f4944 100644 (file)
@@ -171,6 +171,3 @@ each function signature is a contract between the caller and the callee, and the
 This allows for better error messages, better documentation, less unintentional breakages across versions,
 and faster compilation times.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/05_trait_bounds`
index 5b35302..575cc45 100644 (file)
@@ -115,6 +115,3 @@ If a method returns a `&String`, you're promising that there is heap-allocated U
 If a method returns a `&str`, instead, you have a lot more freedom: you're just saying that _somewhere_ there's a
 bunch of text data and that a subset of it matches what you need, therefore you're returning a reference to it.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/06_str_slice`
index 744e515..c70c95a 100644 (file)
@@ -90,6 +90,3 @@ is defined on both `T` and `U`, which one will be called?
 
 We'll examine later in the course the "safest" use cases for deref coercion: smart pointers.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/07_deref`
index c360a9c..7ff8054 100644 (file)
@@ -78,6 +78,3 @@ All the types we've seen so far are `Sized`: `u32`, `String`, `bool`, etc.
 `&str` is `Sized` though! We know its size at compile time: two `usize`s, one for the pointer
 and one for the length.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/08_sized`
index fd8a4f3..7800399 100644 (file)
@@ -139,6 +139,3 @@ In most cases, the target type is either:
 
 `.into()` will work out of the box as long as the compiler can infer the target type from the context without ambiguity.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/09_from`
index f7cec03..f72ccf1 100644 (file)
@@ -141,10 +141,6 @@ To recap:
 - Use a **generic parameter** when you want to allow multiple implementations of the trait for the same type,
   with different input types.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/10_assoc_vs_generic`
-
 [^flexible]: Flexibility is rarely free: the trait definition is more complex due to `Output`, and implementors have to reason about
 what they want to return. The trade-off is only justified if that flexibility is actually needed. Keep that in mind
 when designing your own traits.
index 1383409..404e836 100644 (file)
@@ -106,6 +106,3 @@ The compiler implements `Clone` for `MyType` as you would expect: it clones each
 then constructs a new `MyType` instance using the cloned fields.\
 Remember that you can use `cargo expand` (or your IDE) to explore the code generated by `derive` macros.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/11_clone`
index 5d425d9..d9b18dd 100644 (file)
@@ -112,6 +112,3 @@ struct MyStruct {
 }
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/12_copy`
index c6c2527..2fdc251 100644 (file)
@@ -51,6 +51,3 @@ error[E0184]: the trait `Copy` cannot be implemented for this type; the type has
   |                 ^^^^ `Copy` not allowed on types with destructors
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/13_drop`
index a4573dc..3504b49 100644 (file)
@@ -26,6 +26,3 @@ A few guidelines to keep in mind:
 Before moving on, let's go through one last exercise to consolidate what we've learned.
 You'll have minimal guidance this timeā€”just the exercise description and the tests to guide you.
 
-## References
-
-- The exercise for this section is located in `exercises/04_traits/14_outro`
index 25cc6a3..2a71c42 100644 (file)
@@ -14,6 +14,3 @@ We'll need to introduce a few more concepts along the way:
 - The `TryFrom` and `TryInto` traits, for fallible conversions
 - Rust's package system, explaining what's a library, what's a binary, how to use third-party crates
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/00_intro`
index cad7d64..6f665c0 100644 (file)
@@ -42,6 +42,3 @@ enum Status {
 
 `enum`, just like `struct`, defines **a new Rust type**.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/01_enum`
index da209c1..6df9240 100644 (file)
@@ -69,6 +69,3 @@ match status {
 
 The `_` pattern matches anything that wasn't matched by the previous patterns.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/02_match`
index d43967e..55d9ffb 100644 (file)
@@ -87,6 +87,3 @@ match status {
 }
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/03_variants_with_data`
index bdb3cbf..05abcd4 100644 (file)
@@ -65,6 +65,3 @@ Both `if let` and `let/else` are idiomatic Rust constructs.\
 Use them as you see fit to improve the readability of your code,
 but don't overdo it: `match` is always there when you need it.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/04_if_let`
index 53acc23..b8ddf6e 100644 (file)
@@ -73,6 +73,3 @@ assert_eq!(second.2, 8);
 
 Tuples are a convenient way of grouping values together when you can't be bothered to define a dedicated struct type.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/05_nullability`
index 333d38c..1781f72 100644 (file)
@@ -82,6 +82,3 @@ That's the big advantage of `Result`: it makes fallibility explicit.
 Keep in mind, though, that panics exist. They aren't tracked by the type system, just like exceptions in other languages.
 But they're meant for **unrecoverable errors** and should be used sparingly.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/06_fallibility`
index f83f802..8786d75 100644 (file)
@@ -39,6 +39,3 @@ When you call a function that returns a `Result`, you have two key options:
   }
   ```
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/07_unwrap`
index 180a2dd..d4452fa 100644 (file)
@@ -37,6 +37,3 @@ match s.parse_u32() {
 }
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/08_error_enums`
index c99ea76..66f1777 100644 (file)
@@ -51,6 +51,3 @@ while `Debug` provides a low-level representation that's more suitable to develo
 That's why `Debug` can be automatically implemented using the `#[derive(Debug)]` attribute, while `Display`
 **requires** a manual implementation.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/09_error_trait`
index ca1c12a..ab4d13d 100644 (file)
@@ -62,6 +62,3 @@ binary crate inside. If you want to create a library crate instead, you can use
 cargo new my-library --lib
 ```
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/10_packages`
index f1c2766..e325e18 100644 (file)
@@ -53,6 +53,3 @@ static_assertions = "1.1.0"
 
 We've been using a few of these throughout the book to shorten our tests.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/11_dependencies`
index 74aff5f..22d3d36 100644 (file)
@@ -40,6 +40,3 @@ In the case of `thiserror`, we have:
 - `#[error("{0}")]`: this is the syntax to define a `Display` implementation for each variant of the custom error type.
   `{0}` is replaced by the zero-th field of the variant (`String`, in this case) when the error is displayed.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/12_thiserror`
index c0a0342..dac28d5 100644 (file)
@@ -39,6 +39,3 @@ being attempted.
 Just like `From` and `Into`, `TryFrom` and `TryInto` are dual traits.\
 If you implement `TryFrom` for a type, you get `TryInto` for free.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/13_try_from`
index 680e018..8c18480 100644 (file)
@@ -149,6 +149,3 @@ You can use the `?` operator to shorten your error handling code significantly.\
 In particular, the `?` operator will automatically convert the error type of the fallible operation into the error type
 of the function, if a conversion is possible (i.e. if there is a suitable `From` implementation)
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/14_source`
index 1949135..a4040e9 100644 (file)
@@ -12,10 +12,6 @@ as long as they have a `TicketTitle`, they know it's valid **by construction**.
 
 This is just an example of how you can use Rust's type system to make your code safer and more expressive.
 
-## References
-
-- The exercise for this section is located in `exercises/05_ticket_v2/15_outro`
-
 ## Further reading
 
 - [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)
index 2c5a72b..f3469e4 100644 (file)
@@ -77,8 +77,5 @@ You can think of `'_` as a **placeholder** for the lifetime of the `&self` refer
 See the [References](#references) section for a link to the official documentation on lifetime elision.\
 In most cases, you can rely on the compiler telling you when you need to add explicit lifetime annotations.
 
-## References
-
-- [std::vec::Vec::iter](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.iter)
 - [std::slice::Iter](https://doc.rust-lang.org/std/slice/struct.Iter.html)
 - [Lifetime elision rules](https://doc.rust-lang.org/reference/lifetime-elision.html)
diff --git a/helpers/mdbook-exercise-linker/Cargo.toml b/helpers/mdbook-exercise-linker/Cargo.toml
new file mode 100644 (file)
index 0000000..26574ee
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name = "mdbook-exercise-linker"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.86"
+clap = "4.5.4"
+mdbook = "0.4.40"
+semver = "1.0.23"
+serde_json = "1.0.117"
diff --git a/helpers/mdbook-exercise-linker/src/lib.rs b/helpers/mdbook-exercise-linker/src/lib.rs
new file mode 100644 (file)
index 0000000..0f4686e
--- /dev/null
@@ -0,0 +1,72 @@
+use anyhow::{Context, Error};
+use mdbook::book::Book;
+use mdbook::preprocess::{Preprocessor, PreprocessorContext};
+use mdbook::BookItem;
+
+/// A no-op preprocessor.
+pub struct ExerciseLinker;
+
+impl ExerciseLinker {
+    pub fn new() -> ExerciseLinker {
+        ExerciseLinker
+    }
+}
+
+impl Preprocessor for ExerciseLinker {
+    fn name(&self) -> &str {
+        "exercise-linker"
+    }
+
+    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
+        let config = ctx
+            .config
+            .get_preprocessor(self.name())
+            .context("Failed to get preprocessor configuration")?;
+        let key = String::from("exercise_root_url");
+        let root_url = config
+            .get(&key)
+            .context("Failed to get `exercise_root_url`")?;
+        let root_url = root_url
+            .as_str()
+            .context("`exercise_root_url` is not a string")?
+            .to_owned();
+
+        book.sections
+            .iter_mut()
+            .for_each(|i| process_book_item(i, &root_url));
+        Ok(book)
+    }
+
+    fn supports_renderer(&self, _renderer: &str) -> bool {
+        true
+    }
+}
+
+fn process_book_item(item: &mut BookItem, root_url: &str) {
+    match item {
+        BookItem::Chapter(chapter) => {
+            chapter.sub_items.iter_mut().for_each(|item| {
+                process_book_item(item, root_url);
+            });
+
+            let Some(source_path) = &chapter.source_path else {
+                return;
+            };
+            let source_path = source_path.display().to_string();
+
+            // Ignore non-exercise chapters
+            if !source_path.chars().take(2).all(|c| c.is_digit(10)) {
+                return;
+            }
+
+            let exercise_path = source_path.strip_suffix(".md").unwrap();
+            let link_section = format!(
+                    "\n## Exercise\n\nThe exercise for this section is located in [`{exercise_path}`]({})",
+                    format!("{}/{}", root_url, exercise_path)
+                );
+            chapter.content.push_str(&link_section);
+        }
+        BookItem::Separator => {}
+        BookItem::PartTitle(_) => {}
+    }
+}
diff --git a/helpers/mdbook-exercise-linker/src/main.rs b/helpers/mdbook-exercise-linker/src/main.rs
new file mode 100644 (file)
index 0000000..ac4b52f
--- /dev/null
@@ -0,0 +1,67 @@
+use std::io;
+use std::process;
+
+use clap::{Arg, ArgMatches, Command};
+use mdbook::errors::Error;
+use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
+use semver::{Version, VersionReq};
+
+use mdbook_exercise_linker::ExerciseLinker;
+
+pub fn make_app() -> Command {
+    Command::new("exercise-linker").subcommand(
+        Command::new("supports")
+            .arg(Arg::new("renderer").required(true))
+            .about("Check whether a renderer is supported by this preprocessor"),
+    )
+}
+
+fn main() {
+    let matches = make_app().get_matches();
+
+    // Users will want to construct their own preprocessor here
+    let preprocessor = ExerciseLinker::new();
+
+    if let Some(sub_args) = matches.subcommand_matches("supports") {
+        handle_supports(&preprocessor, sub_args);
+    } else if let Err(e) = handle_preprocessing(&preprocessor) {
+        eprintln!("{}", e);
+        process::exit(1);
+    }
+}
+
+fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
+    let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
+
+    let book_version = Version::parse(&ctx.mdbook_version)?;
+    let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
+
+    if !version_req.matches(&book_version) {
+        eprintln!(
+            "Warning: The {} plugin was built against version {} of mdbook, \
+             but we're being called from version {}",
+            pre.name(),
+            mdbook::MDBOOK_VERSION,
+            ctx.mdbook_version
+        );
+    }
+
+    let processed_book = pre.run(&ctx, book)?;
+    serde_json::to_writer(io::stdout(), &processed_book)?;
+
+    Ok(())
+}
+
+fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
+    let renderer = sub_args
+        .get_one::<String>("renderer")
+        .expect("Required argument");
+    let supported = pre.supports_renderer(renderer);
+
+    // Signal whether the renderer is supported by exiting with 1 or 0.
+    if supported {
+        process::exit(0);
+    } else {
+        process::exit(1);
+    }
+}