├── .envrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── crate │ ├── Cargo.toml │ ├── README.md │ ├── docs │ │ └── SUMMARY.md │ └── src │ │ └── lib.rs └── workspace │ ├── Cargo.toml │ └── src │ └── lib.rs ├── flake.lock ├── flake.nix ├── include-utils-macro ├── Cargo.toml └── src │ ├── include_location.rs │ └── lib.rs ├── rustfmt.toml ├── src └── lib.rs ├── taplo.toml ├── tests ├── data │ ├── anchor.md │ ├── markdown_with_header.md │ └── sample.md └── main.rs └── treefmt.nix /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | lints: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: cachix/install-nix-action@v25 11 | with: 12 | nix_path: nixpkgs=channel:nixos-unstable 13 | - uses: cachix/cachix-action@v14 14 | with: 15 | name: nixpkgs-cross-overlay 16 | 17 | - name: "Check formatting" 18 | run: nix flake check 19 | 20 | - name: "Run linters" 21 | run: nix run .#ci-lints 22 | 23 | - name: "Run semver checks" 24 | run: nix run .#ci-semver-checks 25 | 26 | tests: 27 | strategy: 28 | matrix: 29 | os: [ubuntu-latest, macos-latest] 30 | 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: cachix/install-nix-action@v25 35 | with: 36 | nix_path: nixpkgs=channel:nixos-unstable 37 | - uses: cachix/cachix-action@v14 38 | with: 39 | name: nixpkgs-cross-overlay 40 | 41 | - name: "Run cargo tests" 42 | run: nix run .#ci-tests 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | /Cargo.lock 21 | 22 | # Nix files 23 | .direnv/ 24 | **/result/ 25 | 26 | # IDE 27 | .vscode/ 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## Unreleased 10 | 11 | ## [0.2.4] 04.12.2024 12 | 13 | - Fix typos in the documentation. 14 | 15 | - Bump minimum supported Rust version to `1.78`. 16 | 17 | ## [0.2.3] 04.11.2024 18 | 19 | - Replace unmaintained `proc-macro-error` crate by the `manyhow` one. 20 | 21 | ## [0.2.2] 18.04.2024 22 | 23 | - Update `homepage` to `repository` in cargo manifest files. 24 | 25 | ## [0.2.1] 27.03.2024 26 | 27 | - Fix lints 28 | 29 | ## [0.2.0] 30 | 31 | - Improved workspace support. 32 | 33 | If the `workspace` feature is enabled, then if the file cannot be found 34 | relative to the `CARGO_MANIFEST_DIR`, it will be searched relative to the 35 | cargo workspace root directory. 36 | 37 | ## [0.1.1] 38 | 39 | - Fix lints and `include-utils` crate category. 40 | 41 | ## [0.1.0] 42 | 43 | - First public release 44 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "camino" 7 | version = "1.1.9" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 10 | dependencies = [ 11 | "serde", 12 | ] 13 | 14 | [[package]] 15 | name = "cargo-platform" 16 | version = "0.1.8" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" 19 | dependencies = [ 20 | "serde", 21 | ] 22 | 23 | [[package]] 24 | name = "cargo_metadata" 25 | version = "0.19.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" 28 | dependencies = [ 29 | "camino", 30 | "cargo-platform", 31 | "semver", 32 | "serde", 33 | "serde_json", 34 | "thiserror", 35 | ] 36 | 37 | [[package]] 38 | name = "either" 39 | version = "1.13.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 42 | 43 | [[package]] 44 | name = "example-include-crate" 45 | version = "0.2.4" 46 | dependencies = [ 47 | "include-utils", 48 | ] 49 | 50 | [[package]] 51 | name = "example-include-workspace" 52 | version = "0.2.4" 53 | dependencies = [ 54 | "include-utils", 55 | ] 56 | 57 | [[package]] 58 | name = "include-utils" 59 | version = "0.2.4" 60 | dependencies = [ 61 | "include-utils-macro", 62 | ] 63 | 64 | [[package]] 65 | name = "include-utils-macro" 66 | version = "0.2.4" 67 | dependencies = [ 68 | "cargo_metadata", 69 | "itertools", 70 | "manyhow", 71 | "proc-macro2", 72 | "quote", 73 | "syn", 74 | ] 75 | 76 | [[package]] 77 | name = "itertools" 78 | version = "0.13.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 81 | dependencies = [ 82 | "either", 83 | ] 84 | 85 | [[package]] 86 | name = "itoa" 87 | version = "1.0.14" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 90 | 91 | [[package]] 92 | name = "manyhow" 93 | version = "0.11.4" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" 96 | dependencies = [ 97 | "manyhow-macros", 98 | "proc-macro2", 99 | "quote", 100 | "syn", 101 | ] 102 | 103 | [[package]] 104 | name = "manyhow-macros" 105 | version = "0.11.4" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" 108 | dependencies = [ 109 | "proc-macro-utils", 110 | "proc-macro2", 111 | "quote", 112 | ] 113 | 114 | [[package]] 115 | name = "memchr" 116 | version = "2.7.4" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 119 | 120 | [[package]] 121 | name = "proc-macro-utils" 122 | version = "0.10.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" 125 | dependencies = [ 126 | "proc-macro2", 127 | "quote", 128 | "smallvec", 129 | ] 130 | 131 | [[package]] 132 | name = "proc-macro2" 133 | version = "1.0.92" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 136 | dependencies = [ 137 | "unicode-ident", 138 | ] 139 | 140 | [[package]] 141 | name = "quote" 142 | version = "1.0.37" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 145 | dependencies = [ 146 | "proc-macro2", 147 | ] 148 | 149 | [[package]] 150 | name = "ryu" 151 | version = "1.0.18" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 154 | 155 | [[package]] 156 | name = "semver" 157 | version = "1.0.23" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 160 | dependencies = [ 161 | "serde", 162 | ] 163 | 164 | [[package]] 165 | name = "serde" 166 | version = "1.0.215" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 169 | dependencies = [ 170 | "serde_derive", 171 | ] 172 | 173 | [[package]] 174 | name = "serde_derive" 175 | version = "1.0.215" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 178 | dependencies = [ 179 | "proc-macro2", 180 | "quote", 181 | "syn", 182 | ] 183 | 184 | [[package]] 185 | name = "serde_json" 186 | version = "1.0.133" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 189 | dependencies = [ 190 | "itoa", 191 | "memchr", 192 | "ryu", 193 | "serde", 194 | ] 195 | 196 | [[package]] 197 | name = "smallvec" 198 | version = "1.13.2" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 201 | 202 | [[package]] 203 | name = "syn" 204 | version = "2.0.90" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 207 | dependencies = [ 208 | "proc-macro2", 209 | "quote", 210 | "unicode-ident", 211 | ] 212 | 213 | [[package]] 214 | name = "thiserror" 215 | version = "2.0.4" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" 218 | dependencies = [ 219 | "thiserror-impl", 220 | ] 221 | 222 | [[package]] 223 | name = "thiserror-impl" 224 | version = "2.0.4" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" 227 | dependencies = [ 228 | "proc-macro2", 229 | "quote", 230 | "syn", 231 | ] 232 | 233 | [[package]] 234 | name = "unicode-ident" 235 | version = "1.0.14" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 238 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["examples/crate", "examples/workspace", "include-utils-macro"] 3 | resolver = "2" 4 | 5 | # Common workspace metadata. 6 | [workspace.package] 7 | version = "0.2.4" 8 | authors = ["Aleksey Sidorov "] 9 | documentation = "https://docs.rs/include-utils" 10 | edition = "2021" 11 | repository = "https://github.com/alekseysidorov/include-utils" 12 | license = "MIT OR Apache-2.0" 13 | rust-version = "1.78" 14 | 15 | [workspace.dependencies] 16 | include-utils = { path = ".", version = "0.2.4", default-features = false } 17 | include-utils-macro = { path = "include-utils-macro", version = "0.2.4" } 18 | 19 | cargo_metadata = "0.19.1" 20 | itertools = "0.13" 21 | pretty_assertions = "1.4.1" 22 | proc-macro2 = "1.0" 23 | quote = "1.0" 24 | syn = "2.0.90" 25 | manyhow = "0.11" 26 | 27 | [workspace.lints.rust] 28 | missing_docs = "warn" 29 | missing_debug_implementations = "warn" 30 | unsafe_code = "deny" 31 | 32 | [workspace.lints.rustdoc] 33 | broken_intra_doc_links = "deny" 34 | 35 | [workspace.lints.clippy] 36 | pedantic = { level = "warn", priority = -1 } 37 | module_name_repetitions = "allow" 38 | 39 | # Root package 40 | [package] 41 | name = "include-utils" 42 | version.workspace = true 43 | edition.workspace = true 44 | description = "mdBook-like include macro as the powerful replacement for the standard `include_str` macro." 45 | 46 | authors.workspace = true 47 | categories = ["development-tools"] 48 | documentation.workspace = true 49 | repository.workspace = true 50 | keywords = ["no_std", "documentation", "rustdoc"] 51 | license.workspace = true 52 | rust-version.workspace = true 53 | 54 | [dependencies] 55 | include-utils-macro = { workspace = true } 56 | 57 | [features] 58 | default = [] 59 | # Enable looking up to the workspace root directory for the included files in addition to the 60 | # `CARGO_MANIFEST_PATH`. 61 | workspace = ["include-utils-macro/workspace"] 62 | 63 | [lints] 64 | workspace = true 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aleksey Sidorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Include utils 2 | 3 | [![tests](https://github.com/alekseysidorov/include-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/alekseysidorov/include-utils/actions/workflows/ci.yml) 4 | [![crates.io](https://img.shields.io/crates/v/include-utils.svg)](https://crates.io/crates/include-utils) 5 | [![Documentation](https://docs.rs/include-utils/badge.svg)](https://docs.rs/include-utils) 6 | [![MIT/Apache-2 licensed](https://img.shields.io/crates/l/include-utils)](./LICENSE) 7 | 8 | A more powerful replacement for the standard `include_str` macros. 9 | 10 | 11 | 12 | Often you only need a specific part of the file, e.g. relevant lines for an 13 | example, or section of README.md. This crate provides macros that can include 14 | only part of a file, similar to the [mdbook] specific feature. 15 | 16 | Imagine that you want to include "usage" section from your repository readme 17 | file to the crate documentation. But you do not want to see in crate 18 | documentation some parts of readme file, like header, badges, etc. With the 19 | [`include_str`] macro you can only include the entire file content. 20 | 21 | But with the [`include_md`] macro you can include only a specific section of the 22 | file. 23 | 24 | ## Notes 25 | 26 | - Unlike the built-in macro, this macros uses the `CARGO_MANIFEST_DIR` as the 27 | current directory instead of the directory from which macro is called. 28 | 29 | - If the `workspace` feature is enabled, then if the file cannot be found 30 | relative to the `CARGO_MANIFEST_DIR`, it will be searched relative to the 31 | cargo workspace root directory. It may be useful if you want to store your 32 | documentation in the single directory outside the crates. In this case you 33 | have to copy included directory to each crate before you publish it to the 34 | crates registry. 35 | 36 | ## Usage 37 | 38 | ```rust 39 | //! # Crate overview 40 | //! 41 | #![doc = include_utils::include_md!("README.md:description")] 42 | //! 43 | //! ## Other section 44 | ``` 45 | 46 | [mdbook]: https://rust-lang.github.io/mdBook/format/mdbook.html#including-portions-of-a-file 47 | 48 | 49 | 50 | [`include_str`]: https://doc.rust-lang.org/stable/std/macro.include_str.html 51 | [`include_md`]: https://docs.rs/include-utils/latest/include_utils/macro.include_md.html 52 | -------------------------------------------------------------------------------- /examples/crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-include-crate" 3 | 4 | rust-version.workspace = true 5 | version.workspace = true 6 | authors.workspace = true 7 | documentation.workspace = true 8 | edition.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | resolver = "2" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | include-utils = { workspace = true, default-features = false } 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /examples/crate/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | -------------------------------------------------------------------------------- /examples/crate/docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | A summary example 4 | -------------------------------------------------------------------------------- /examples/crate/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Overview 2 | //! 3 | //! Example of including a file from the crate root even in workspace. 4 | //! 5 | //! ## Summary 6 | #![doc = include_utils::include_md!("docs/SUMMARY.md:3")] 7 | //! 8 | -------------------------------------------------------------------------------- /examples/workspace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-include-workspace" 3 | 4 | rust-version.workspace = true 5 | version.workspace = true 6 | authors.workspace = true 7 | documentation.workspace = true 8 | edition.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | publish = false 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | include-utils = { workspace = true, features = ["workspace"] } 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /examples/workspace/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Overview 2 | //! 3 | //! Example of including a file from the workspace root. 4 | //! 5 | //! ## Changelog 6 | #![doc = include_utils::include_md!("CHANGELOG.md")] 7 | //! 8 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1733120037, 24 | "narHash": "sha256-En+gSoVJ3iQKPDU1FHrR6zIxSLXKjzKY+pnh9tt+Yts=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "f9f0d5c5380be0a599b1fb54641fa99af8281539", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-24.11", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1731890469, 40 | "narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=", 41 | "owner": "nixos", 42 | "repo": "nixpkgs", 43 | "rev": "5083ec887760adfe12af64830a66807423a859a7", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nixos", 48 | "ref": "nixpkgs-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "rust-overlay": "rust-overlay", 58 | "treefmt-nix": "treefmt-nix" 59 | } 60 | }, 61 | "rust-overlay": { 62 | "inputs": { 63 | "nixpkgs": [ 64 | "nixpkgs" 65 | ] 66 | }, 67 | "locked": { 68 | "lastModified": 1733279627, 69 | "narHash": "sha256-NCNDAGPkdFdu+DLErbmNbavmVW9AwkgP7azROFFSB0U=", 70 | "owner": "oxalica", 71 | "repo": "rust-overlay", 72 | "rev": "4da5a80ef76039e80468c902f1e9f5c0eab87d96", 73 | "type": "github" 74 | }, 75 | "original": { 76 | "owner": "oxalica", 77 | "repo": "rust-overlay", 78 | "type": "github" 79 | } 80 | }, 81 | "systems": { 82 | "locked": { 83 | "lastModified": 1681028828, 84 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 85 | "owner": "nix-systems", 86 | "repo": "default", 87 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 88 | "type": "github" 89 | }, 90 | "original": { 91 | "owner": "nix-systems", 92 | "repo": "default", 93 | "type": "github" 94 | } 95 | }, 96 | "treefmt-nix": { 97 | "inputs": { 98 | "nixpkgs": "nixpkgs_2" 99 | }, 100 | "locked": { 101 | "lastModified": 1733222881, 102 | "narHash": "sha256-JIPcz1PrpXUCbaccEnrcUS8jjEb/1vJbZz5KkobyFdM=", 103 | "owner": "numtide", 104 | "repo": "treefmt-nix", 105 | "rev": "49717b5af6f80172275d47a418c9719a31a78b53", 106 | "type": "github" 107 | }, 108 | "original": { 109 | "owner": "numtide", 110 | "repo": "treefmt-nix", 111 | "type": "github" 112 | } 113 | } 114 | }, 115 | "root": "root", 116 | "version": 7 117 | } 118 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; 4 | rust-overlay = { 5 | url = "github:oxalica/rust-overlay"; 6 | inputs.nixpkgs.follows = "nixpkgs"; 7 | }; 8 | treefmt-nix.url = "github:numtide/treefmt-nix"; 9 | flake-utils.url = "github:numtide/flake-utils"; 10 | }; 11 | 12 | outputs = 13 | { self 14 | , nixpkgs 15 | , flake-utils 16 | , rust-overlay 17 | , treefmt-nix 18 | }: flake-utils.lib.eachDefaultSystem (system: 19 | let 20 | # Setup nixpkgs 21 | pkgs = import nixpkgs { 22 | inherit system; 23 | 24 | overlays = [ 25 | rust-overlay.overlays.default 26 | (final: prev: { 27 | rustToolchains = { 28 | msrv = prev.rust-bin.stable."1.78.0".default; 29 | stable = prev.rust-bin.stable.latest.default.override { 30 | extensions = [ 31 | "rust-src" 32 | "rust-analyzer" 33 | ]; 34 | }; 35 | nightly = prev.rust-bin.nightly.latest.default; 36 | }; 37 | }) 38 | ]; 39 | }; 40 | # Setup runtime dependencies 41 | runtimeInputs = with pkgs; [ 42 | cargo-nextest 43 | openssl 44 | pkg-config 45 | ] 46 | # Some additional libraries for the Darwin platform 47 | ++ lib.optionals stdenv.isDarwin [ 48 | darwin.apple_sdk.frameworks.SystemConfiguration 49 | ]; 50 | 51 | # Eval the treefmt modules from ./treefmt.nix 52 | treefmt = (treefmt-nix.lib.evalModule pkgs ./treefmt.nix).config.build; 53 | # CI scripts 54 | ci = with pkgs; { 55 | tests = writeShellApplication { 56 | name = "ci-run-tests"; 57 | runtimeInputs = with pkgs; [ rustToolchains.msrv ] ++ runtimeInputs; 58 | text = '' 59 | cargo nextest run --workspace --all-targets --no-default-features 60 | cargo nextest run --workspace --all-targets --all-features 61 | 62 | cargo test --workspace --doc --no-default-features 63 | cargo test --workspace --doc --all-features 64 | ''; 65 | }; 66 | 67 | lints = writeShellApplication { 68 | name = "ci-run-lints"; 69 | runtimeInputs = with pkgs; [ rustToolchains.stable typos ] ++ runtimeInputs; 70 | text = '' 71 | typos 72 | cargo clippy --workspace --all --no-default-features 73 | cargo clippy --workspace --all --all-targets --all-features 74 | cargo doc --workspace --no-deps --no-default-features 75 | cargo doc --workspace --no-deps --all-features 76 | ''; 77 | }; 78 | 79 | semver_checks = writeShellApplication { 80 | name = "ci-run-semver-checks"; 81 | runtimeInputs = with pkgs; [ 82 | rustToolchains.msrv 83 | cargo-semver-checks 84 | ] ++ runtimeInputs; 85 | text = ''cargo semver-checks''; 86 | }; 87 | 88 | # Run them all together 89 | all = writeShellApplication { 90 | name = "ci-run-all"; 91 | runtimeInputs = [ ci.lints ci.tests ]; 92 | text = '' 93 | ci-run-lints 94 | ci-run-tests 95 | ci-run-semver-checks 96 | ''; 97 | }; 98 | }; 99 | 100 | mkCommand = shell: command: 101 | pkgs.writeShellApplication { 102 | name = "cmd-${shell}-${command}"; 103 | runtimeInputs = [ pkgs.nix ]; 104 | text = ''nix develop ".#${shell}" --command "${command}"''; 105 | }; 106 | 107 | mkCommandDefault = mkCommand "default"; 108 | in 109 | { 110 | # for `nix fmt` 111 | formatter = treefmt.wrapper; 112 | # for `nix flake check` 113 | checks.formatting = treefmt.check self; 114 | 115 | devShells.default = pkgs.mkShell { 116 | nativeBuildInputs = with pkgs; runtimeInputs ++ [ 117 | rustToolchains.stable 118 | ci.all 119 | ci.lints 120 | ci.tests 121 | ci.semver_checks 122 | ]; 123 | }; 124 | 125 | # Nightly compilator to run miri tests 126 | devShells.nightly = pkgs.mkShell { 127 | nativeBuildInputs = with pkgs; [ 128 | rustToolchains.nightly 129 | ]; 130 | }; 131 | 132 | packages = { 133 | ci-lints = mkCommandDefault "ci-run-lints"; 134 | ci-tests = mkCommandDefault "ci-run-tests"; 135 | ci-semver-checks = mkCommandDefault "ci-run-semver-checks"; 136 | ci-all = mkCommandDefault "ci-run-all"; 137 | git-install-hooks = pkgs.writeShellScriptBin "install-git-hook" 138 | '' 139 | echo "-> Installing pre-commit hook" 140 | echo "nix flake check" >> "$PWD/.git/hooks/pre-commit" 141 | chmod +x "$PWD/.git/hooks/pre-commit" 142 | 143 | echo "-> Installing pre-push hook" 144 | echo "nix run \".#ci-all\"" >> "$PWD/.git/hooks/pre-push" 145 | chmod +x "$PWD/.git/hooks/pre-push" 146 | ''; 147 | }; 148 | }); 149 | } 150 | -------------------------------------------------------------------------------- /include-utils-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "include-utils-macro" 3 | version.workspace = true 4 | edition.workspace = true 5 | description = "Internal proc macro for the `include-utils` crate." 6 | 7 | authors.workspace = true 8 | documentation.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | rust-version.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | cargo_metadata = { workspace = true, optional = true } 18 | itertools = { workspace = true } 19 | manyhow = { workspace = true } 20 | proc-macro2 = { workspace = true } 21 | quote = { workspace = true } 22 | syn = { workspace = true } 23 | 24 | [features] 25 | default = [] 26 | workspace = ["dep:cargo_metadata"] 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /include-utils-macro/src/include_location.rs: -------------------------------------------------------------------------------- 1 | /// Include location part of the given file path. 2 | #[derive(Debug, PartialEq, Eq)] 3 | pub struct IncludeLocation<'a> { 4 | /// File path itself. 5 | pub path: &'a str, 6 | /// Range of file lines to include. 7 | pub range: IncludeRange<'a>, 8 | } 9 | 10 | /// Include range specification follows the mdbook include portion of file chapter. 11 | /// 12 | /// 13 | #[derive(Debug, PartialEq, Eq)] 14 | pub enum IncludeRange<'a> { 15 | /// Include the entire file content as is. 16 | Full, 17 | /// Include the specific file part. 18 | Range { 19 | from: Option, 20 | to: Option, 21 | }, 22 | /// Include part of file between anchor begin and end. 23 | Anchor { name: &'a str }, 24 | } 25 | 26 | impl IncludeRange<'_> { 27 | fn line(num: usize) -> Self { 28 | Self::Range { 29 | from: Some(num), 30 | to: Some(num + 1), 31 | } 32 | } 33 | 34 | fn left(num: usize) -> Self { 35 | Self::Range { 36 | from: None, 37 | to: Some(num), 38 | } 39 | } 40 | 41 | fn right(num: usize) -> Self { 42 | Self::Range { 43 | from: Some(num), 44 | to: None, 45 | } 46 | } 47 | 48 | fn range(from: usize, to: usize) -> Self { 49 | Self::Range { 50 | from: Some(from), 51 | to: Some(to), 52 | } 53 | } 54 | } 55 | 56 | impl<'a> IncludeLocation<'a> { 57 | pub fn parse(s: &'a str) -> manyhow::Result { 58 | let parts = s.split(':').collect::>(); 59 | 60 | let value = match &parts[..] { 61 | // file.md 62 | [path] => Self { 63 | path, 64 | range: IncludeRange::Full, 65 | }, 66 | // file.md:5 | file.md:component 67 | [path, line] => { 68 | let range = if let Ok(num) = line.parse() { 69 | IncludeRange::line(num) 70 | } else { 71 | IncludeRange::Anchor { name: line } 72 | }; 73 | 74 | Self { path, range } 75 | } 76 | // file.md::2 77 | [path, "", second] => { 78 | let to = second.parse().map_err(|err| { 79 | manyhow::error_message!("unable to parse 'to' include range component. {err}") 80 | })?; 81 | Self { 82 | path, 83 | range: IncludeRange::left(to), 84 | } 85 | } 86 | 87 | // file.md:2: 88 | [path, first, ""] => { 89 | let from = first.parse().map_err(|err| { 90 | manyhow::error_message!("unable to parse 'from' include range component. {err}") 91 | })?; 92 | Self { 93 | path, 94 | range: IncludeRange::right(from), 95 | } 96 | } 97 | 98 | // file.md:2:10 99 | [path, first, second] => { 100 | let from = first.parse().map_err(|err| { 101 | manyhow::error_message!("unable to parse 'from' include range component. {err}") 102 | })?; 103 | let to = second.parse().map_err(|err| { 104 | manyhow::error_message!("unable to parse 'to' include range component. {err}") 105 | })?; 106 | 107 | Self { 108 | path, 109 | range: IncludeRange::range(from, to), 110 | } 111 | } 112 | 113 | _ => manyhow::bail!("unsupported include range layout"), 114 | }; 115 | Ok(value) 116 | } 117 | } 118 | 119 | #[test] 120 | fn test_parse_include_location() { 121 | let test_cases = [ 122 | ( 123 | "file.rs", 124 | IncludeLocation { 125 | path: "file.rs", 126 | range: IncludeRange::Full, 127 | }, 128 | ), 129 | ( 130 | "file.rs:2", 131 | IncludeLocation { 132 | path: "file.rs", 133 | range: IncludeRange::Range { 134 | from: Some(2), 135 | to: Some(3), 136 | }, 137 | }, 138 | ), 139 | ( 140 | "file.rs::10", 141 | IncludeLocation { 142 | path: "file.rs", 143 | range: IncludeRange::Range { 144 | from: None, 145 | to: Some(10), 146 | }, 147 | }, 148 | ), 149 | ( 150 | "file.rs:2:", 151 | IncludeLocation { 152 | path: "file.rs", 153 | range: IncludeRange::Range { 154 | from: Some(2), 155 | to: None, 156 | }, 157 | }, 158 | ), 159 | ( 160 | "file.rs:2:10", 161 | IncludeLocation { 162 | path: "file.rs", 163 | range: IncludeRange::Range { 164 | from: Some(2), 165 | to: Some(10), 166 | }, 167 | }, 168 | ), 169 | ( 170 | "file.rs:component", 171 | IncludeLocation { 172 | path: "file.rs", 173 | range: IncludeRange::Anchor { name: "component" }, 174 | }, 175 | ), 176 | ]; 177 | 178 | for (path, expected) in test_cases { 179 | let actual = IncludeLocation::parse(path).unwrap(); 180 | assert_eq!(actual, expected); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /include-utils-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Internal proc macro for the `include-utils` crate. 2 | 3 | #![allow(missing_docs)] 4 | 5 | use std::path::{Path, PathBuf}; 6 | 7 | use itertools::Itertools; 8 | use manyhow::manyhow; 9 | use proc_macro2::TokenStream as TokenStream2; 10 | use quote::quote; 11 | use syn::LitStr; 12 | 13 | use crate::include_location::{IncludeLocation, IncludeRange}; 14 | 15 | mod include_location; 16 | 17 | #[manyhow] 18 | #[proc_macro] 19 | pub fn include_str_part(file: LitStr) -> manyhow::Result { 20 | let file = file.value(); 21 | let location = IncludeLocation::parse(&file)?; 22 | let file_content = read_file(location.path)?; 23 | 24 | let processed_content = process_file(file_content, &location.range, |_content, _name| { 25 | Err(manyhow::error_message!("Anchors is not supported for the plain string").into()) 26 | })?; 27 | 28 | Ok(quote!(#processed_content)) 29 | } 30 | 31 | #[manyhow] 32 | #[proc_macro] 33 | pub fn include_md(file: LitStr) -> manyhow::Result { 34 | let file = file.value(); 35 | let location = IncludeLocation::parse(&file)?; 36 | let file_content = read_file(location.path)?; 37 | 38 | // TODO Use markdown parser to analyze comments. 39 | let processed_content = process_file(file_content, &location.range, |content, anchor_name| { 40 | let anchor_begin = format!(" 31 | //! An example of anchored section in markdown file. 32 | //! 33 | //! ``` 34 | //! 35 | //! [mdbook]: https://rust-lang.github.io/mdBook/format/mdbook.html#including-portions-of-a-file 36 | 37 | /// Includes a markdown file as a string. 38 | /// 39 | /// See [module][self] documentation. 40 | pub use include_utils_macro::include_md; 41 | /// Includes a part of UTF-8 encoded file as a string. 42 | /// 43 | /// _**Note!** Anchors is not supported by this macro, use specific `include_md` macro to 44 | /// include markdown file section._ 45 | /// 46 | /// See [module][self] documentation. 47 | pub use include_utils_macro::include_str_part; 48 | -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | include = ["**/*.toml"] 2 | 3 | [[rule]] 4 | include = ["**/Cargo.toml"] 5 | keys = ["dependencies", "*-dependencies"] 6 | 7 | [rule.formatting] 8 | reorder_keys = true 9 | -------------------------------------------------------------------------------- /tests/data/anchor.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | all text 4 | 5 | 6 | 7 | conclusion 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/data/markdown_with_header.md: -------------------------------------------------------------------------------- 1 | # Some needless header 2 | 3 | Some needless line 4 | 5 | ```rust 6 | fn main() { 7 | 8 | } 9 | ``` 10 | 11 | ## Some other doc part 12 | 13 | 1. The cat purred contentedly on my lap. 14 | 2. The rain poured down on the city streets. 15 | 3. She ate a sandwich for lunch. 16 | 4. He played soccer with his friends in the park. 17 | 5. The dog barked loudly at the mailman. 18 | -------------------------------------------------------------------------------- /tests/data/sample.md: -------------------------------------------------------------------------------- 1 | 1. First line 2 | 2. Second line 3 | 3. Third line 4 | 4. Fourth line 5 | 5. Fifth line 6 | 6. Sixty line 7 | 7. Seventy line 8 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | //! Tests for include-utils-macro 2 | 3 | use include_utils_macro::{include_md, include_str_part}; 4 | 5 | #[test] 6 | fn test_include_str_line() { 7 | assert_eq!( 8 | include_str_part!("tests/data/markdown_with_header.md:3"), 9 | "Some needless line" 10 | ); 11 | } 12 | 13 | #[test] 14 | fn test_include_str_part() { 15 | assert_eq!( 16 | include_str_part!("tests/data/markdown_with_header.md:5:9"), 17 | "```rust\nfn main() {\n \n}\n```" 18 | ); 19 | } 20 | 21 | #[test] 22 | fn test_include_str_begin() { 23 | assert_eq!( 24 | include_str_part!("tests/data/markdown_with_header.md::3"), 25 | "# Some needless header\n\nSome needless line" 26 | ); 27 | } 28 | 29 | #[test] 30 | fn test_include_str_end() { 31 | assert_eq!( 32 | include_str_part!("tests/data/markdown_with_header.md:16:"), 33 | "4. He played soccer with his friends in the park.\n5. The dog barked loudly at the mailman." 34 | ); 35 | } 36 | 37 | #[test] 38 | fn test_include_md_anchor_all() { 39 | assert_eq!( 40 | include_md!("tests/data/anchor.md:all"), 41 | "all text\n\n\n\nconclusion\n\n" 42 | ); 43 | } 44 | 45 | #[test] 46 | fn test_include_md_anchor_conclusion() { 47 | assert_eq!(include_md!("tests/data/anchor.md:conclusion"), "conclusion"); 48 | } 49 | -------------------------------------------------------------------------------- /treefmt.nix: -------------------------------------------------------------------------------- 1 | # treefmt.nix 2 | { pkgs, ... }: 3 | { 4 | # Used to find the project root 5 | projectRootFile = "flake.nix"; 6 | 7 | programs.nixpkgs-fmt.enable = true; 8 | programs.rustfmt = { 9 | enable = true; 10 | package = pkgs.rustToolchains.nightly; 11 | }; 12 | programs.beautysh.enable = true; 13 | programs.deno.enable = true; 14 | programs.taplo.enable = true; 15 | } 16 | --------------------------------------------------------------------------------