├── .github ├── dependabot.yml └── workflows │ ├── audit.yml │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-Apache ├── LICENSE-MIT ├── README.md ├── check_release.sh ├── clippy.toml ├── custom-format-macros ├── Cargo.toml ├── LICENSE-Apache ├── LICENSE-MIT └── src │ ├── fmt │ ├── mod.rs │ ├── output.rs │ ├── parse.rs │ ├── process.rs │ └── utils.rs │ └── lib.rs ├── custom-format-tests ├── Cargo.toml └── src │ ├── main.rs │ └── tests.rs ├── examples └── strftime.rs ├── rustfmt.toml └── src ├── compile_time.rs ├── lib.rs └── runtime.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "cargo" 9 | directory: "/custom-format-macros" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 0" 6 | 7 | jobs: 8 | audit: 9 | name: "Audit" 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Rust 17 | id: actions-rs 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - name: Audit 25 | run: | 26 | cargo install cargo-audit 27 | cargo audit 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | schedule: 9 | - cron: "0 0 * * 0" 10 | 11 | jobs: 12 | check: 13 | name: "Check" 14 | 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Rust 21 | id: actions-rs 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | profile: minimal 25 | toolchain: stable 26 | override: true 27 | components: rustfmt, clippy 28 | 29 | - name: Format 30 | run: cargo fmt --all -- --check 31 | 32 | - name: Lint 33 | run: cargo clippy -- -D warnings 34 | 35 | doc: 36 | name: "Doc" 37 | 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | 43 | - name: Install Rust 44 | id: actions-rs 45 | uses: actions-rs/toolchain@v1 46 | with: 47 | profile: minimal 48 | toolchain: nightly 49 | override: true 50 | 51 | - name: Doc 52 | run: RUSTDOCFLAGS="-D warnings --cfg docsrs" cargo doc --all-features --no-deps 53 | 54 | test: 55 | strategy: 56 | matrix: 57 | rust: [1.56, stable, nightly] 58 | 59 | name: 'Test/${{ matrix.rust }}/Features="${{ matrix.features }}"' 60 | 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Install Rust 67 | id: actions-rs 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | profile: minimal 71 | toolchain: ${{ matrix.rust }} 72 | override: true 73 | 74 | - name: Test 75 | run: | 76 | cargo test 77 | sh -c "cd custom-format-macros && cargo test" 78 | sh -c "cd custom-format-tests && cargo test" 79 | env: 80 | CARGO_NET_GIT_FETCH_WITH_CLI: true 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | Cargo.lock 3 | target 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["custom-format-macros", "custom-format-tests"] 3 | 4 | [package] 5 | name = "custom-format" 6 | version = "0.3.1" 7 | edition = "2021" 8 | authors = ["x-hgg-x"] 9 | repository = "https://github.com/x-hgg-x/custom-format" 10 | description = "Custom formatting for Rust." 11 | license = "MIT OR Apache-2.0" 12 | keywords = ["no-std", "format", "string", "fmt", "macro"] 13 | categories = ["no-std", "rust-patterns", "value-formatting"] 14 | readme = "README.md" 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | rustdoc-args = ["--cfg", "docsrs"] 19 | 20 | [dependencies] 21 | custom-format-macros = { version = "0.3.1", path = "custom-format-macros" } 22 | 23 | [features] 24 | compile-time = [] 25 | runtime = [] 26 | default = ["compile-time", "runtime"] 27 | -------------------------------------------------------------------------------- /LICENSE-Apache: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 x-hgg-x 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 | # custom-format 2 | 3 | [![version](https://img.shields.io/crates/v/custom-format?color=blue&style=flat-square)](https://crates.io/crates/custom-format) 4 | ![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.56+-important?logo=rust "Minimum Supported Rust Version") 5 | [![Documentation](https://docs.rs/custom-format/badge.svg)](https://docs.rs/custom-format) 6 | 7 | This crate extends the standard formatting syntax with custom format specifiers, by providing custom formatting macros. 8 | 9 | It uses ` :` (a space and a colon) as a separator before the format specifier, which is not a syntax currently accepted and allows supporting standard specifiers in addition to custom specifiers. It also supports [format args capture](https://blog.rust-lang.org/2022/01/13/Rust-1.58.0.html#captured-identifiers-in-format-strings) even on older versions of Rust, since it manually adds the named parameter if missing. 10 | 11 | This library comes in two flavors, corresponding to the following features: 12 | 13 | - `compile-time` (*enabled by default*) 14 | 15 | The set of possible custom format specifiers is defined at compilation, so invalid specifiers can be checked at compile-time. 16 | This allows the library to have the same performance as when using the standard library formatting traits. 17 | 18 | - `runtime` (*enabled by default*) 19 | 20 | The formatting method dynamically checks the format specifier at runtime for each invocation. 21 | This is a slower version, but it has additional flexibility. 22 | 23 | ## Documentation 24 | 25 | Documentation is hosted on [docs.rs](https://docs.rs/custom-format/latest/). 26 | 27 | ## Example 28 | 29 |
30 | Code 31 | 32 | ```rust 33 | use custom_format as cfmt; 34 | 35 | use core::fmt; 36 | 37 | pub struct DateTime { 38 | year: i32, 39 | month: u8, 40 | month_day: u8, 41 | hour: u8, 42 | minute: u8, 43 | second: u8, 44 | nanoseconds: u32, 45 | } 46 | 47 | macro_rules! impl_custom_format_for_datetime { 48 | (match spec { $($spec:literal => $func:expr $(,)?)* }) => { 49 | use cfmt::compile_time::{spec, CustomFormat}; 50 | $( 51 | impl CustomFormat<{ spec($spec) }> for DateTime { 52 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 53 | ($func as fn(&Self, &mut fmt::Formatter) -> fmt::Result)(self, f) 54 | } 55 | } 56 | )* 57 | }; 58 | } 59 | 60 | // Static format specifiers, checked at compile-time 61 | impl_custom_format_for_datetime!(match spec { 62 | // Year with pad for at least 4 digits 63 | "%Y" => |this, f| write!(f, "{:04}", this.year), 64 | // Year % 100 (00..99) 65 | "%y" => |this, f| write!(f, "{:02}", (this.year % 100).abs()), 66 | // Month of the year, zero-padded (01..12) 67 | "%m" => |this, f| write!(f, "{:02}", this.month), 68 | // Day of the month, zero-padded (01..31) 69 | "%d" => |this, f| write!(f, "{:02}", this.month_day), 70 | // Hour of the day, 24-hour clock, zero-padded (00..23) 71 | "%H" => |this, f| write!(f, "{:02}", this.hour), 72 | // Minute of the hour (00..59) 73 | "%M" => |this, f| write!(f, "{:02}", this.minute), 74 | // Second of the minute (00..60) 75 | "%S" => |this, f| write!(f, "{:02}", this.second), 76 | // Date (%m/%d/%y) 77 | "%D" => { 78 | |this, f| { 79 | let month = cfmt::custom_formatter!("%m", this); 80 | let day = cfmt::custom_formatter!("%d", this); 81 | let year = cfmt::custom_formatter!("%y", this); 82 | write!(f, "{}/{}/{}", month, day, year) 83 | } 84 | } 85 | }); 86 | 87 | // Dynamic format specifiers, checked at runtime 88 | impl cfmt::runtime::CustomFormat for DateTime { 89 | fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result { 90 | let mut chars = spec.chars(); 91 | match (chars.next(), chars.next_back()) { 92 | // Nanoseconds with n digits (%nN) 93 | (Some('%'), Some('N')) => match chars.as_str().parse() { 94 | Ok(n) if n > 0 => { 95 | if n <= 9 { 96 | write!(f, "{:0width$}", self.nanoseconds / 10u32.pow(9 - n as u32), width = n) 97 | } else { 98 | write!(f, "{:09}{:0width$}", self.nanoseconds, 0, width = n - 9) 99 | } 100 | } 101 | _ => Err(fmt::Error), 102 | }, 103 | _ => Err(fmt::Error), 104 | } 105 | } 106 | } 107 | 108 | let dt = DateTime { 109 | year: 1836, 110 | month: 5, 111 | month_day: 18, 112 | hour: 23, 113 | minute: 45, 114 | second: 54, 115 | nanoseconds: 123456789, 116 | }; 117 | 118 | // Expands to: 119 | // 120 | // match (&("DateTime"), &dt) { 121 | // (arg0, arg1) => ::std::println!( 122 | // "The {0:?} is: {1}-{2}-{3} {4}:{5}:{6}.{7}", 123 | // arg0, 124 | // ::custom_format::custom_formatter!("%Y", arg1), 125 | // ::custom_format::custom_formatter!("%m", arg1), 126 | // ::custom_format::custom_formatter!("%d", arg1), 127 | // ::custom_format::custom_formatter!("%H", arg1), 128 | // ::custom_format::custom_formatter!("%M", arg1), 129 | // ::custom_format::custom_formatter!("%S", arg1), 130 | // ::custom_format::runtime::CustomFormatter::new("%6N", arg1) 131 | // ), 132 | // } 133 | // 134 | // Output: `The "DateTime" is: 1836-05-18 23:45:54.123456` 135 | // 136 | // The custom format specifier is interpreted as a compile-time specifier by default, 137 | // or as a runtime specifier if it is inside "<>". 138 | cfmt::println!( 139 | "The {ty:?} is: {dt :%Y}-{dt :%m}-{dt :%d} {dt :%H}:{dt :%M}:{dt :%S}.{dt :<%6N>}", 140 | ty = "DateTime", 141 | ); 142 | 143 | // Compile-time error since "%h" is not a valid format specifier 144 | // cfmt::println!("{0 :%h}", dt); 145 | 146 | // Panic at runtime since "%h" is not a valid format specifier 147 | // cfmt::println!("{0 :<%h>}", dt); 148 | ``` 149 | 150 |
151 | 152 | ## Compiler support 153 | 154 | Requires `rustc 1.56+`. 155 | 156 | ## License 157 | 158 | This project is licensed under either of 159 | 160 | - [Apache License, Version 2.0](https://github.com/x-hgg-x/custom-format/blob/master/LICENSE-Apache) 161 | - [MIT license](https://github.com/x-hgg-x/custom-format/blob/master/LICENSE-MIT) 162 | 163 | at your option. 164 | 165 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in 166 | this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 167 | additional terms or conditions. 168 | -------------------------------------------------------------------------------- /check_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | run() { 4 | RUSTC=$1 5 | 6 | cargo +$RUSTC test 7 | sh -c "cd custom-format-macros && cargo +$RUSTC test" 8 | sh -c "cd custom-format-tests && cargo +$RUSTC test" 9 | } 10 | 11 | run 1.56 12 | run stable 13 | run nightly 14 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.56" 2 | -------------------------------------------------------------------------------- /custom-format-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom-format-macros" 3 | version = "0.3.1" 4 | edition = "2021" 5 | authors = ["x-hgg-x"] 6 | repository = "https://github.com/x-hgg-x/custom-format" 7 | description = "Procedural macros for the custom-format crate." 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["format", "string", "fmt", "macro"] 10 | categories = ["rust-patterns", "value-formatting"] 11 | readme = "../README.md" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | litrs = { version = "0.4.0", default-features = false } 18 | unicode-ident = "1.0" 19 | 20 | [dev-dependencies] 21 | proc-macro2 = "1.0" 22 | unicode-normalization = { version = "0.1.22", default-features = false } 23 | -------------------------------------------------------------------------------- /custom-format-macros/LICENSE-Apache: -------------------------------------------------------------------------------- 1 | ../LICENSE-Apache -------------------------------------------------------------------------------- /custom-format-macros/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /custom-format-macros/src/fmt/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module containing procedural macros common code. 2 | 3 | mod output; 4 | mod parse; 5 | mod process; 6 | mod utils; 7 | 8 | use output::*; 9 | use process::*; 10 | 11 | #[cfg(not(test))] 12 | use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; 13 | #[cfg(test)] 14 | use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; 15 | 16 | /// Error type for the procedural macro 17 | type Error = std::borrow::Cow<'static, str>; 18 | 19 | /// Separator for custom format specifier 20 | const CUSTOM_SEPARATOR: &str = " :"; 21 | 22 | /// Proc-macro argument 23 | #[derive(Debug)] 24 | struct Argument { 25 | /// Optional identifier name 26 | ident: Option, 27 | /// Expression 28 | expr: Group, 29 | } 30 | 31 | /// Parsed input elements 32 | #[derive(Debug)] 33 | struct ParsedInput { 34 | /// Crate identifier (`$crate`) 35 | crate_ident: Ident, 36 | /// Root macro tokens 37 | root_macro: TokenStream, 38 | /// First argument tokens 39 | first_arg: Option, 40 | /// List of proc-macro arguments 41 | arguments: Vec, 42 | /// Span of the format string 43 | span: Span, 44 | } 45 | 46 | /// Identifier normalized in Unicode NFC 47 | #[derive(Debug, PartialEq)] 48 | struct Id<'a>(&'a str); 49 | 50 | impl<'a> Id<'a> { 51 | /// Construct a new [`Id`] value 52 | fn new(name: &'a str) -> Result { 53 | #[cfg(not(test))] 54 | let normalized_name = Ident::new(name, Span::call_site()).to_string(); 55 | #[cfg(test)] 56 | let normalized_name = unicode_normalization::UnicodeNormalization::nfc(name).collect::(); 57 | 58 | if name == normalized_name { 59 | Ok(Self(name)) 60 | } else { 61 | Err(format!("identifiers in format string must be normalized in Unicode NFC (`{:?}` != `{:?}`)", name, normalized_name)) 62 | } 63 | } 64 | 65 | /// Return the identifier value 66 | fn name(&self) -> &'a str { 67 | self.0 68 | } 69 | } 70 | 71 | /// Kind of a proc-macro argument 72 | #[derive(Debug, PartialEq)] 73 | enum ArgKind<'a> { 74 | /// Positional argument 75 | Positional(usize), 76 | /// Named argument 77 | Named(Id<'a>), 78 | } 79 | 80 | /// Standard count format specifier 81 | #[derive(Debug, PartialEq)] 82 | enum Count<'a> { 83 | /// Count is provided by an argument 84 | Argument(ArgKind<'a>), 85 | /// Count is provided by an integer 86 | Integer(&'a str), 87 | } 88 | 89 | /// Standard precision format specifier 90 | #[derive(Debug, PartialEq)] 91 | enum Precision<'a> { 92 | /// Precision is provided by the next positional argument 93 | Asterisk, 94 | /// Precision is provided by the specified count 95 | WithCount(Count<'a>), 96 | } 97 | 98 | /// Custom format specifier 99 | #[derive(Debug, Copy, Clone, PartialEq)] 100 | enum Spec<'a> { 101 | /// Format specifier checked at compile-time 102 | CompileTime(&'a str), 103 | /// Format specifier checked at runtime 104 | Runtime(&'a str), 105 | } 106 | 107 | /// Piece of a format string 108 | #[derive(Debug, PartialEq)] 109 | enum Piece<'a> { 110 | /// Standard format specifier data 111 | StdFmt { 112 | /// Kind of the positional argument 113 | arg_kind_position: ArgKind<'a>, 114 | /// Optional kind of the width argument 115 | arg_kind_width: Option>, 116 | /// Optional kind of the precision argument 117 | arg_kind_precision: Option>, 118 | }, 119 | /// Custom format specifier data 120 | CustomFmt { 121 | /// Kind of the positional argument 122 | arg_kind: ArgKind<'a>, 123 | /// Custom format specifier 124 | spec: Spec<'a>, 125 | }, 126 | } 127 | 128 | /// Processed elements of the format string pieces 129 | #[derive(Debug)] 130 | struct ProcessedPieces<'a> { 131 | /// Argument indices associated to the format string pieces, with custom format specifiers if applicable 132 | arg_indices: Vec<(usize, Option>)>, 133 | /// List of new arguments to be added from captured identifiers in the format string, if not already existing 134 | new_args: Vec<&'a str>, 135 | } 136 | 137 | /// Create tokens representing a compilation error 138 | fn compile_error(msg: &str, span: Span) -> TokenStream { 139 | let mut tokens = vec![ 140 | TokenTree::from(Ident::new("compile_error", span)), 141 | TokenTree::from(Punct::new('!', Spacing::Alone)), 142 | TokenTree::from(Group::new(Delimiter::Parenthesis, TokenTree::from(Literal::string(msg)).into())), 143 | ]; 144 | 145 | for t in &mut tokens { 146 | t.set_span(span); 147 | } 148 | 149 | tokens.into_iter().collect() 150 | } 151 | 152 | /// Main function, working with both [`proc_macro::TokenStream`] and `proc_macro2::TokenStream` 153 | pub(crate) fn fmt(input: TokenStream) -> TokenStream { 154 | let (format_string, parsed_input) = match parse_tokens(input) { 155 | Err(compile_error) => return compile_error, 156 | Ok(x) => x, 157 | }; 158 | 159 | let (new_format_string, pieces) = match parse_format_string(&format_string) { 160 | Err(error) => return compile_error(&error, parsed_input.span), 161 | Ok(x) => x, 162 | }; 163 | 164 | let processed_pieces = match process_pieces(pieces, &parsed_input.arguments) { 165 | Err(error) => return compile_error(&error, parsed_input.span), 166 | Ok(x) => x, 167 | }; 168 | 169 | compute_output(parsed_input, &new_format_string, processed_pieces) 170 | } 171 | -------------------------------------------------------------------------------- /custom-format-macros/src/fmt/output.rs: -------------------------------------------------------------------------------- 1 | //! Functions used for computing output tokens. 2 | 3 | use super::*; 4 | 5 | /// Push `::` to the list of token trees 6 | fn push_two_colons(v: &mut Vec) { 7 | v.push(Punct::new(':', Spacing::Joint).into()); 8 | v.push(Punct::new(':', Spacing::Alone).into()); 9 | } 10 | 11 | /// Push `$crate::custom_formatter!` to the list of token trees 12 | fn push_compile_time_formatter(v: &mut Vec, crate_ident: &Ident) { 13 | v.push(crate_ident.clone().into()); 14 | push_two_colons(v); 15 | v.push(Ident::new("custom_formatter", Span::call_site()).into()); 16 | v.push(Punct::new('!', Spacing::Alone).into()); 17 | } 18 | 19 | /// Push `$crate::runtime::CustomFormatter::new` to the list of token trees 20 | fn push_runtime_formatter(v: &mut Vec, crate_ident: &Ident) { 21 | v.push(crate_ident.clone().into()); 22 | push_two_colons(v); 23 | v.push(Ident::new("runtime", Span::call_site()).into()); 24 | push_two_colons(v); 25 | v.push(Ident::new("CustomFormatter", Span::call_site()).into()); 26 | push_two_colons(v); 27 | v.push(Ident::new("new", Span::call_site()).into()); 28 | } 29 | 30 | /// Push the whole macro call to the list of token trees 31 | fn push_macro_call( 32 | v: &mut Vec, 33 | crate_ident: Ident, 34 | root_macro: TokenStream, 35 | first_arg: Option, 36 | new_format_string: &str, 37 | arg_indices: Vec<(usize, Option)>, 38 | args: &[TokenStream], 39 | ) { 40 | v.extend(root_macro); 41 | 42 | v.push(TokenTree::from(Group::new(Delimiter::Parenthesis, { 43 | let mut fmt_args = Vec::::new(); 44 | 45 | if let Some(first_arg) = first_arg { 46 | fmt_args.extend(first_arg); 47 | fmt_args.push(Punct::new(',', Spacing::Alone).into()); 48 | } 49 | 50 | fmt_args.push(TokenTree::from(Literal::string(new_format_string))); 51 | 52 | for (index, spec) in arg_indices { 53 | fmt_args.push(Punct::new(',', Spacing::Alone).into()); 54 | 55 | match spec { 56 | None => fmt_args.extend(args[index].clone()), 57 | Some(spec) => { 58 | let spec_literal = match spec { 59 | Spec::CompileTime(spec) => { 60 | push_compile_time_formatter(&mut fmt_args, &crate_ident); 61 | Literal::string(spec) 62 | } 63 | Spec::Runtime(spec) => { 64 | push_runtime_formatter(&mut fmt_args, &crate_ident); 65 | Literal::string(spec) 66 | } 67 | }; 68 | 69 | fmt_args.push(TokenTree::from(Group::new(Delimiter::Parenthesis, { 70 | let mut stream = vec![spec_literal.into(), Punct::new(',', Spacing::Alone).into()]; 71 | stream.extend(args[index].clone()); 72 | stream.into_iter().collect() 73 | }))); 74 | } 75 | } 76 | } 77 | 78 | fmt_args.into_iter().collect() 79 | }))); 80 | } 81 | 82 | /// Compute output Rust code 83 | pub(super) fn compute_output(parsed_input: ParsedInput, new_format_string: &str, processed_pieces: ProcessedPieces) -> TokenStream { 84 | let ParsedInput { crate_ident, root_macro, first_arg, arguments, span } = parsed_input; 85 | let ProcessedPieces { arg_indices, new_args } = processed_pieces; 86 | 87 | let arg_exprs: Vec = arguments 88 | .into_iter() 89 | .map(|arg| arg.expr.into()) 90 | .chain(new_args.into_iter().map(|name| Ident::new(name, span).into())) 91 | .map(|tt| vec![TokenTree::from(Punct::new('&', Spacing::Alone)), tt].into_iter().collect()) 92 | .collect(); 93 | 94 | let arg_idents: Vec = 95 | (0..arg_exprs.len()).map(|index| TokenTree::from(Ident::new(&format!("arg{}", index), Span::call_site())).into()).collect(); 96 | 97 | // Don't use a `match` for the `format_args!` macro because it creates temporary values 98 | if let Some(TokenTree::Ident(ident)) = root_macro.clone().into_iter().nth(5) { 99 | if &ident.to_string() == "format_args" { 100 | let mut output = Vec::new(); 101 | push_macro_call(&mut output, crate_ident, root_macro, first_arg, new_format_string, arg_indices, &arg_exprs); 102 | return output.into_iter().collect(); 103 | } 104 | } 105 | 106 | let mut output = vec![Ident::new("match", Span::call_site()).into()]; 107 | 108 | output.push(TokenTree::from(Group::new(Delimiter::Parenthesis, { 109 | let mut exprs = Vec::new(); 110 | 111 | for arg in arg_exprs { 112 | exprs.extend(arg); 113 | exprs.push(Punct::new(',', Spacing::Alone).into()); 114 | } 115 | 116 | exprs.pop(); 117 | exprs.into_iter().collect() 118 | }))); 119 | 120 | output.push(TokenTree::from(Group::new(Delimiter::Brace, { 121 | let mut block = Vec::new(); 122 | 123 | block.push(TokenTree::from(Group::new(Delimiter::Parenthesis, { 124 | let mut arm_pat = Vec::new(); 125 | 126 | for arg_ident in &arg_idents { 127 | arm_pat.extend(arg_ident.clone()); 128 | arm_pat.push(Punct::new(',', Spacing::Alone).into()); 129 | } 130 | 131 | arm_pat.pop(); 132 | arm_pat.into_iter().collect() 133 | }))); 134 | 135 | block.push(Punct::new('=', Spacing::Joint).into()); 136 | block.push(Punct::new('>', Spacing::Alone).into()); 137 | 138 | push_macro_call(&mut block, crate_ident, root_macro, first_arg, new_format_string, arg_indices, &arg_idents); 139 | 140 | block.push(Punct::new(',', Spacing::Alone).into()); 141 | 142 | block.into_iter().collect() 143 | }))); 144 | 145 | output.into_iter().collect() 146 | } 147 | 148 | #[cfg(test)] 149 | mod test { 150 | use super::*; 151 | 152 | #[test] 153 | fn test_compute_output() -> Result<(), Box> { 154 | let create_argument = |name: Option<&str>, s| { 155 | let expr = Group::new(Delimiter::Parenthesis, TokenTree::from(Literal::string(s)).into()); 156 | Argument { ident: name.map(|x| x.to_owned()), expr } 157 | }; 158 | 159 | let data = [ 160 | ( 161 | "::std::println!", 162 | concat!( 163 | r#"match (&("0"), &("1"), &("2"), &("3"), &h, &g) { (arg0, arg1, arg2, arg3, arg4, arg5) => "#, 164 | r#"::std::println!("{0}, {1}, {2}, {3}, {4}, {5}, {6:.7$}, {8:9$}", arg4, "#, 165 | r#"crate::custom_formatter!("%z", arg4), arg1, arg1, arg3, "#, 166 | r#"crate::runtime::CustomFormatter::new("%x", arg2), arg1, arg0, arg3, arg5), }"# 167 | ), 168 | ), 169 | ( 170 | "::core::format_args!", 171 | concat!( 172 | r#"::core::format_args!("{0}, {1}, {2}, {3}, {4}, {5}, {6:.7$}, {8:9$}", &h, "#, 173 | r#"crate::custom_formatter!("%z", &h), &("1"), &("1"), &("3"), "#, 174 | r#"crate::runtime::CustomFormatter::new("%x", &("2")), "#, 175 | r#"&("1"), &("0"), &("3"), &g)"#, 176 | ), 177 | ), 178 | ]; 179 | 180 | for &(root_macro, result) in &data { 181 | let new_format_string = "{0}, {1}, {2}, {3}, {4}, {5}, {6:.7$}, {8:9$}"; 182 | 183 | let arguments = vec![create_argument(None, "0"), create_argument(Some("a"), "1"), create_argument(Some("b"), "2"), create_argument(Some("c"), "3")]; 184 | 185 | let arg_indices = vec![ 186 | (4, None), 187 | (4, Some(Spec::CompileTime("%z"))), 188 | (1, None), 189 | (1, None), 190 | (3, None), 191 | (2, Some(Spec::Runtime("%x"))), 192 | (1, None), 193 | (0, None), 194 | (3, None), 195 | (5, None), 196 | ]; 197 | 198 | let new_args = vec!["h", "g"]; 199 | 200 | let output = compute_output( 201 | ParsedInput { 202 | crate_ident: Ident::new("crate", Span::call_site()), 203 | root_macro: root_macro.parse()?, 204 | first_arg: None, 205 | arguments, 206 | span: Span::call_site(), 207 | }, 208 | new_format_string, 209 | ProcessedPieces { arg_indices, new_args }, 210 | ); 211 | 212 | assert_eq!(output.to_string(), result.parse::()?.to_string()); 213 | } 214 | 215 | Ok(()) 216 | } 217 | 218 | #[test] 219 | fn test_compute_output_with_first_arg() -> Result<(), Box> { 220 | let output = compute_output( 221 | ParsedInput { 222 | crate_ident: Ident::new("crate", Span::call_site()), 223 | root_macro: "::std::writeln!".parse()?, 224 | first_arg: Some("f".parse()?), 225 | arguments: vec![], 226 | span: Span::call_site(), 227 | }, 228 | "string", 229 | ProcessedPieces { arg_indices: vec![], new_args: vec![] }, 230 | ); 231 | 232 | assert_eq!(output.to_string(), "match () { () => ::std::writeln!(f, \"string\"), }".parse::()?.to_string()); 233 | 234 | Ok(()) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /custom-format-macros/src/fmt/parse.rs: -------------------------------------------------------------------------------- 1 | //! Functions used for parsing standard format specifier. 2 | 3 | use super::utils::StrCursor; 4 | use super::{ArgKind, Count, Error, Id, Precision}; 5 | 6 | /// Process standard fill and alignment specifiers 7 | pub(super) fn process_align(cursor: &mut StrCursor) -> [Option; 2] { 8 | let cursor0 = cursor.clone(); 9 | let c1 = cursor.next(); 10 | let cursor1 = cursor.clone(); 11 | let c2 = cursor.next(); 12 | 13 | if c1.is_some() && matches!(c2, Some('<') | Some('^') | Some('>')) { 14 | [c1, c2] 15 | } else if matches!(c1, Some('<') | Some('^') | Some('>')) { 16 | *cursor = cursor1; 17 | [c1, None] 18 | } else { 19 | *cursor = cursor0; 20 | [None, None] 21 | } 22 | } 23 | 24 | /// Process standard sign specifier 25 | pub(super) fn process_sign(cursor: &mut StrCursor) -> Option { 26 | let old_cursor = cursor.clone(); 27 | 28 | match cursor.next() { 29 | sign @ Some('+') | sign @ Some('-') => sign, 30 | _ => { 31 | *cursor = old_cursor; 32 | None 33 | } 34 | } 35 | } 36 | 37 | /// Process standard alternate specifier 38 | pub(super) fn process_alternate(cursor: &mut StrCursor) -> Option { 39 | let old_cursor = cursor.clone(); 40 | 41 | match cursor.next() { 42 | sign @ Some('#') => sign, 43 | _ => { 44 | *cursor = old_cursor; 45 | None 46 | } 47 | } 48 | } 49 | 50 | /// Process standard sign-aware zero-padding specifier 51 | pub(super) fn process_sign_aware_zero_pad(cursor: &mut StrCursor) -> Option { 52 | let old_cursor = cursor.clone(); 53 | let c = cursor.next(); 54 | let next = cursor.remaining().bytes().next(); 55 | 56 | match (c, next) { 57 | (sign @ Some('0'), next) if next != Some(b'$') => sign, 58 | _ => { 59 | *cursor = old_cursor; 60 | None 61 | } 62 | } 63 | } 64 | 65 | /// Process standard width specifier 66 | pub(super) fn process_width<'a>(cursor: &mut StrCursor<'a>) -> Result>, Error> { 67 | process_count(cursor) 68 | } 69 | 70 | /// Process standard precision specifier 71 | pub(super) fn process_precision<'a>(cursor: &mut StrCursor<'a>) -> Result>, Error> { 72 | let mut old_cursor = cursor.clone(); 73 | 74 | if !matches!(cursor.next(), Some('.')) { 75 | *cursor = old_cursor; 76 | return Ok(None); 77 | } 78 | 79 | old_cursor = cursor.clone(); 80 | 81 | match cursor.next() { 82 | Some('*') => Ok(Some(Precision::Asterisk)), 83 | _ => { 84 | *cursor = old_cursor; 85 | match process_count(cursor)? { 86 | Some(count) => Ok(Some(Precision::WithCount(count))), 87 | None => Err("invalid count in format string".into()), 88 | } 89 | } 90 | } 91 | } 92 | 93 | /// Process standard count specifier 94 | pub(super) fn process_count<'a>(cursor: &mut StrCursor<'a>) -> Result>, Error> { 95 | let old_cursor = cursor.clone(); 96 | 97 | // Try parsing as argument with '$' 98 | match parse_argument(cursor)? { 99 | Some(arg_kind) if cursor.next() == Some('$') => return Ok(Some(Count::Argument(arg_kind))), 100 | _ => *cursor = old_cursor, 101 | } 102 | 103 | // Try parsing as integer 104 | match cursor.read_while(|c| c.is_ascii_digit()) { 105 | "" => Ok(None), 106 | integer => Ok(Some(Count::Integer(integer))), 107 | } 108 | } 109 | 110 | /// Parse argument in a format specifier 111 | pub(super) fn parse_argument<'a>(cursor: &mut StrCursor<'a>) -> Result>, Error> { 112 | // Try parsing as integer 113 | let integer_argument = cursor.read_while(|c| c.is_ascii_digit()); 114 | if !integer_argument.is_empty() { 115 | return Ok(Some(ArgKind::Positional(integer_argument.parse().unwrap()))); 116 | } 117 | 118 | // Try parsing as identifier 119 | let old_cursor = cursor.clone(); 120 | let remaining = cursor.remaining(); 121 | 122 | let first_char = match cursor.next() { 123 | Some(first_char) => first_char, 124 | None => return Ok(None), 125 | }; 126 | 127 | let first_char_len = remaining.len() - cursor.remaining().len(); 128 | 129 | let identifier = match first_char { 130 | '_' => match cursor.read_while(unicode_ident::is_xid_continue).len() { 131 | 0 => return Err("invalid argument: argument name cannot be a single underscore".into()), 132 | len => &remaining[..first_char_len + len], 133 | }, 134 | c => { 135 | if unicode_ident::is_xid_start(c) { 136 | let len = cursor.read_while(unicode_ident::is_xid_continue).len(); 137 | &remaining[..first_char_len + len] 138 | } else { 139 | *cursor = old_cursor; 140 | return Ok(None); 141 | } 142 | } 143 | }; 144 | 145 | Ok(Some(ArgKind::Named(Id::new(identifier)?))) 146 | } 147 | 148 | #[cfg(test)] 149 | mod test { 150 | use super::*; 151 | 152 | #[test] 153 | fn test_process_align() { 154 | let data = [ 155 | ("^--", [Some('^'), None], "--"), 156 | ("<--", [Some('<'), None], "--"), 157 | (">--", [Some('>'), None], "--"), 158 | ("-^-", [Some('-'), Some('^')], "-"), 159 | ("-<-", [Some('-'), Some('<')], "-"), 160 | ("->-", [Some('-'), Some('>')], "-"), 161 | ("--^", [None, None], "--^"), 162 | ("--<", [None, None], "--<"), 163 | ("-->", [None, None], "-->"), 164 | ]; 165 | 166 | for &(fmt, output, remaining) in &data { 167 | let mut cursor = StrCursor::new(fmt); 168 | assert_eq!(process_align(&mut cursor), output); 169 | assert_eq!(cursor.remaining(), remaining); 170 | } 171 | } 172 | 173 | #[test] 174 | fn test_process_sign() { 175 | let data = [("+000", Some('+'), "000"), ("-000", Some('-'), "000"), ("0000", None, "0000")]; 176 | 177 | for &(fmt, output, remaining) in &data { 178 | let mut cursor = StrCursor::new(fmt); 179 | assert_eq!(process_sign(&mut cursor), output); 180 | assert_eq!(cursor.remaining(), remaining); 181 | } 182 | } 183 | 184 | #[test] 185 | fn test_process_alternate() { 186 | let data = [("#0", Some('#'), "0"), ("00", None, "00")]; 187 | 188 | for &(fmt, output, remaining) in &data { 189 | let mut cursor = StrCursor::new(fmt); 190 | assert_eq!(process_alternate(&mut cursor), output); 191 | assert_eq!(cursor.remaining(), remaining); 192 | } 193 | } 194 | 195 | #[test] 196 | fn test_process_sign_aware_zero_pad() { 197 | let data = [("0123", Some('0'), "123"), ("0.6", Some('0'), ".6"), ("123", None, "123"), ("0$", None, "0$")]; 198 | 199 | for &(fmt, output, remaining) in &data { 200 | let mut cursor = StrCursor::new(fmt); 201 | assert_eq!(process_sign_aware_zero_pad(&mut cursor), output); 202 | assert_eq!(cursor.remaining(), remaining); 203 | } 204 | } 205 | 206 | #[test] 207 | fn test_parse_argument() -> Result<(), Error> { 208 | let data = [ 209 | ("05sdkfh-", Some(ArgKind::Positional(5)), "sdkfh-"), 210 | ("_sdkfh-", Some(ArgKind::Named(Id::new("_sdkfh")?)), "-"), 211 | ("_é€", Some(ArgKind::Named(Id::new("_é")?)), "€"), 212 | ("é€", Some(ArgKind::Named(Id::new("é")?)), "€"), 213 | ("@é€", None, "@é€"), 214 | ("€", None, "€"), 215 | ]; 216 | 217 | for &(fmt, ref output, remaining) in &data { 218 | let mut cursor = StrCursor::new(fmt); 219 | assert_eq!(parse_argument(&mut cursor)?, *output); 220 | assert_eq!(cursor.remaining(), remaining); 221 | } 222 | 223 | assert_eq!(&*parse_argument(&mut StrCursor::new("_")).unwrap_err(), "invalid argument: argument name cannot be a single underscore"); 224 | 225 | assert_eq!( 226 | &*parse_argument(&mut StrCursor::new("A\u{30a}")).unwrap_err(), 227 | r#"identifiers in format string must be normalized in Unicode NFC (`"A\u{30a}"` != `"Å"`)"# 228 | ); 229 | 230 | Ok(()) 231 | } 232 | 233 | #[test] 234 | fn test_process_width() -> Result<(), Error> { 235 | let data = [ 236 | ("05sdkfh$-", Some(Count::Integer("05")), "sdkfh$-"), 237 | ("05$sdkfh-", Some(Count::Argument(ArgKind::Positional(5))), "sdkfh-"), 238 | ("_sdkfh$-", Some(Count::Argument(ArgKind::Named(Id::new("_sdkfh")?))), "-"), 239 | ("_é$€", Some(Count::Argument(ArgKind::Named(Id::new("_é")?))), "€"), 240 | ("é$€", Some(Count::Argument(ArgKind::Named(Id::new("é")?))), "€"), 241 | ("_sdkfh-$", None, "_sdkfh-$"), 242 | ("_é€$", None, "_é€$"), 243 | ("é€$", None, "é€$"), 244 | ("@é€", None, "@é€"), 245 | ("€", None, "€"), 246 | ]; 247 | 248 | for &(fmt, ref output, remaining) in &data { 249 | let mut cursor = StrCursor::new(fmt); 250 | assert_eq!(process_width(&mut cursor)?, *output); 251 | assert_eq!(cursor.remaining(), remaining); 252 | } 253 | 254 | Ok(()) 255 | } 256 | 257 | #[test] 258 | fn test_process_precision() -> Result<(), Error> { 259 | let data = [ 260 | (".*--", Some(Precision::Asterisk), "--"), 261 | (".05sdkfh$-", Some(Precision::WithCount(Count::Integer("05"))), "sdkfh$-"), 262 | (".05$sdkfh-", Some(Precision::WithCount(Count::Argument(ArgKind::Positional(5)))), "sdkfh-"), 263 | ("._sdkfh$-", Some(Precision::WithCount(Count::Argument(ArgKind::Named(Id::new("_sdkfh")?)))), "-"), 264 | ("._é$€", Some(Precision::WithCount(Count::Argument(ArgKind::Named(Id::new("_é")?)))), "€"), 265 | (".é$€", Some(Precision::WithCount(Count::Argument(ArgKind::Named(Id::new("é")?)))), "€"), 266 | ("05sdkfh$-", None, "05sdkfh$-"), 267 | ("05$sdkfh-", None, "05$sdkfh-"), 268 | ("_sdkfh$-", None, "_sdkfh$-"), 269 | ("_é$€", None, "_é$€"), 270 | ("é$€", None, "é$€"), 271 | ("_sdkfh-$", None, "_sdkfh-$"), 272 | ("_é€$", None, "_é€$"), 273 | ("é€$", None, "é€$"), 274 | ("@é€", None, "@é€"), 275 | ("€", None, "€"), 276 | ]; 277 | 278 | for &(fmt, ref output, remaining) in &data { 279 | let mut cursor = StrCursor::new(fmt); 280 | assert_eq!(process_precision(&mut cursor)?, *output); 281 | assert_eq!(cursor.remaining(), remaining); 282 | } 283 | 284 | assert_eq!(process_precision(&mut StrCursor::new("._sdkfh-$")).unwrap_err(), "invalid count in format string"); 285 | assert_eq!(process_precision(&mut StrCursor::new("._é€$")).unwrap_err(), "invalid count in format string"); 286 | assert_eq!(process_precision(&mut StrCursor::new(".é€$")).unwrap_err(), "invalid count in format string"); 287 | assert_eq!(process_precision(&mut StrCursor::new(".@é€")).unwrap_err(), "invalid count in format string"); 288 | assert_eq!(process_precision(&mut StrCursor::new(".€")).unwrap_err(), "invalid count in format string"); 289 | 290 | Ok(()) 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /custom-format-macros/src/fmt/process.rs: -------------------------------------------------------------------------------- 1 | //! Functions used for processing input. 2 | 3 | use super::utils::StrCursor; 4 | use super::*; 5 | 6 | use std::collections::hash_map::{Entry, HashMap}; 7 | use std::fmt::Write; 8 | 9 | /// Parse input tokens 10 | pub(super) fn parse_tokens(input: TokenStream) -> Result<(String, ParsedInput), TokenStream> { 11 | let token_trees: Vec<_> = input.into_iter().collect(); 12 | 13 | let mut args_iter = token_trees.split(|token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == ',' )); 14 | 15 | let crate_ident = match args_iter.next() { 16 | Some([TokenTree::Ident(ident)]) => ident.clone(), 17 | _ => return Err(compile_error("invalid tokens", Span::call_site())), 18 | }; 19 | 20 | // A `$crate` identifier is impossible to construct with `proc_macro2::Ident` 21 | #[cfg(not(test))] 22 | if &crate_ident.to_string() != "$crate" { 23 | return Err(compile_error("invalid tokens", Span::call_site())); 24 | } 25 | 26 | let root_macro = match args_iter.next() { 27 | Some([TokenTree::Group(group)]) => group.stream(), 28 | _ => return Err(compile_error("invalid tokens", Span::call_site())), 29 | }; 30 | 31 | let first_arg = match args_iter.next() { 32 | Some([TokenTree::Group(group)]) => match group.stream() { 33 | stream if !stream.is_empty() => Some(stream), 34 | _ => None, 35 | }, 36 | _ => return Err(compile_error("invalid tokens", Span::call_site())), 37 | }; 38 | 39 | let remaining: Vec<_> = match args_iter.next() { 40 | Some([TokenTree::Group(group)]) => group.stream().into_iter().collect(), 41 | _ => return Err(compile_error("invalid tokens", Span::call_site())), 42 | }; 43 | 44 | let mut remaining_iter = remaining.split(|token| matches!(token, TokenTree::Punct(punct) if punct.as_char() == ',' )); 45 | 46 | let (format_string, span) = match remaining_iter.next() { 47 | Some([TokenTree::Group(group)]) => { 48 | let mut stream_iter = group.stream().into_iter(); 49 | match (stream_iter.next(), stream_iter.next()) { 50 | (Some(tt), None) => { 51 | let span = tt.span(); 52 | match litrs::StringLit::parse(tt.to_string()) { 53 | Ok(lit) => (lit.into_value().into_owned(), span), 54 | Err(e) => return Err(compile_error(&e.to_string(), span)), 55 | } 56 | } 57 | _ => return Err(compile_error("invalid tokens", Span::call_site())), 58 | } 59 | } 60 | _ => return Err(compile_error("invalid tokens", Span::call_site())), 61 | }; 62 | 63 | let arguments = remaining_iter 64 | .map(|x| match x { 65 | [TokenTree::Group(group)] => { 66 | let mut ident = None; 67 | let mut stream = group.stream(); 68 | 69 | let mut stream_iter = stream.clone().into_iter(); 70 | let (tt1, tt2, tt3, tt4) = (stream_iter.next(), stream_iter.next(), stream_iter.next(), stream_iter.next()); 71 | 72 | if let Some(TokenTree::Group(g1)) = tt1 { 73 | let g1_inner = g1.stream().to_string(); 74 | 75 | // Since Rust 1.61: Proc macros no longer see ident matchers wrapped in groups (#92472) 76 | let mut g1_iter = g1_inner.parse::().ok().into_iter().flat_map(|x| x.into_iter()); 77 | 78 | if let (Some(TokenTree::Ident(_)), None) = (g1_iter.next(), g1_iter.next()) { 79 | if let (Some(TokenTree::Punct(punct)), Some(TokenTree::Group(inner_group)), None) = (tt2, tt3, tt4) { 80 | if punct.as_char() == '=' && punct.spacing() == Spacing::Alone { 81 | ident = Some(g1_inner); 82 | stream = inner_group.stream(); 83 | } 84 | } 85 | } 86 | } 87 | 88 | Ok(Argument { ident, expr: Group::new(Delimiter::Parenthesis, stream) }) 89 | } 90 | _ => Err(compile_error("invalid tokens", span)), 91 | }) 92 | .collect::, _>>()?; 93 | 94 | Ok((format_string, ParsedInput { crate_ident, root_macro, first_arg, arguments, span })) 95 | } 96 | 97 | /// Process formatting argument 98 | fn process_fmt<'a>( 99 | fmt: &'a str, 100 | current_positional_index: &mut usize, 101 | new_format_string: &mut String, 102 | new_current_index: &mut usize, 103 | ) -> Result, Error> { 104 | let mut fmt_chars = fmt.chars(); 105 | let inner = match (fmt_chars.next(), fmt_chars.next_back()) { 106 | (Some('{'), Some('}')) => fmt_chars.as_str().trim_end(), 107 | _ => return Err("invalid format string".into()), 108 | }; 109 | 110 | write!(new_format_string, "{{{}", *new_current_index).unwrap(); 111 | *new_current_index += 1; 112 | 113 | let piece = match inner.find(CUSTOM_SEPARATOR) { 114 | Some(position) => { 115 | let specifier = &inner[position + CUSTOM_SEPARATOR.len()..]; 116 | 117 | let mut spec_chars = specifier.chars(); 118 | let spec = match (spec_chars.next(), spec_chars.next_back()) { 119 | (Some('<'), Some('>')) => Spec::Runtime(spec_chars.as_str()), 120 | _ => Spec::CompileTime(specifier), 121 | }; 122 | 123 | let mut cursor = StrCursor::new(&inner[..position]); 124 | 125 | let arg_kind = parse::parse_argument(&mut cursor)?.unwrap_or_else(|| { 126 | let arg_kind = ArgKind::Positional(*current_positional_index); 127 | *current_positional_index += 1; 128 | arg_kind 129 | }); 130 | 131 | if !cursor.remaining().is_empty() { 132 | return Err("invalid format string".into()); 133 | } 134 | 135 | Piece::CustomFmt { arg_kind, spec } 136 | } 137 | None => { 138 | let mut cursor = StrCursor::new(inner); 139 | 140 | let mut has_arg_kind = true; 141 | let mut arg_kind_position = parse::parse_argument(&mut cursor)?.unwrap_or_else(|| { 142 | let arg_kind = ArgKind::Positional(*current_positional_index); 143 | *current_positional_index += 1; 144 | has_arg_kind = false; 145 | arg_kind 146 | }); 147 | 148 | let mut arg_kind_width = None; 149 | let mut arg_kind_precision = None; 150 | 151 | match cursor.next() { 152 | Some(':') => { 153 | new_format_string.push(':'); 154 | new_format_string.extend(parse::process_align(&mut cursor).iter().flatten()); 155 | new_format_string.extend(parse::process_sign(&mut cursor)); 156 | new_format_string.extend(parse::process_alternate(&mut cursor)); 157 | new_format_string.extend(parse::process_sign_aware_zero_pad(&mut cursor)); 158 | 159 | match parse::process_width(&mut cursor)? { 160 | None => (), 161 | Some(Count::Integer(integer)) => *new_format_string += integer, 162 | Some(Count::Argument(arg_kind_for_width)) => { 163 | arg_kind_width = Some(arg_kind_for_width); 164 | write!(new_format_string, "{}$", *new_current_index).unwrap(); 165 | *new_current_index += 1; 166 | } 167 | } 168 | 169 | match parse::process_precision(&mut cursor)? { 170 | None => (), 171 | Some(Precision::Asterisk) => { 172 | let new_arg_kind = ArgKind::Positional(*current_positional_index); 173 | *current_positional_index += 1; 174 | 175 | if has_arg_kind { 176 | arg_kind_precision = Some(new_arg_kind); 177 | } else { 178 | arg_kind_precision = Some(arg_kind_position); 179 | arg_kind_position = new_arg_kind; 180 | } 181 | 182 | write!(new_format_string, ".{}$", *new_current_index).unwrap(); 183 | *new_current_index += 1; 184 | } 185 | Some(Precision::WithCount(Count::Integer(integer))) => write!(new_format_string, ".{}", integer).unwrap(), 186 | Some(Precision::WithCount(Count::Argument(arg_kind_for_precision))) => { 187 | arg_kind_precision = Some(arg_kind_for_precision); 188 | write!(new_format_string, ".{}$", *new_current_index).unwrap(); 189 | *new_current_index += 1; 190 | } 191 | }; 192 | 193 | *new_format_string += cursor.remaining(); 194 | } 195 | None => (), 196 | _ => return Err("invalid format string".into()), 197 | }; 198 | 199 | Piece::StdFmt { arg_kind_position, arg_kind_width, arg_kind_precision } 200 | } 201 | }; 202 | 203 | new_format_string.push('}'); 204 | 205 | Ok(piece) 206 | } 207 | 208 | /// Parse format string 209 | pub(super) fn parse_format_string(format_string: &str) -> Result<(String, Vec), Error> { 210 | let mut cursor = StrCursor::new(format_string); 211 | let mut current_positional_index = 0; 212 | 213 | let mut pieces = Vec::new(); 214 | let mut new_format_string = String::new(); 215 | let mut new_current_index = 0; 216 | 217 | loop { 218 | new_format_string += cursor.read_until(|c| c == '{'); 219 | 220 | if cursor.remaining().is_empty() { 221 | break; 222 | } 223 | 224 | if cursor.remaining().starts_with("{{") { 225 | cursor.next(); 226 | cursor.next(); 227 | new_format_string += "{{"; 228 | continue; 229 | } 230 | 231 | let fmt = cursor.read_until_included(|c| c == '}'); 232 | pieces.push(process_fmt(fmt, &mut current_positional_index, &mut new_format_string, &mut new_current_index)?); 233 | } 234 | 235 | Ok((new_format_string, pieces)) 236 | } 237 | 238 | /// Process list of pieces 239 | pub(super) fn process_pieces<'a>(pieces: Vec>, arguments: &[Argument]) -> Result, Error> { 240 | let mut arguments_iter = arguments.iter(); 241 | arguments_iter.position(|arg| arg.ident.is_some()); 242 | 243 | if !arguments_iter.all(|arg| arg.ident.is_some()) { 244 | return Err("positional arguments cannot follow named arguments".into()); 245 | } 246 | 247 | let mut named_args_positions = HashMap::new(); 248 | for (index, arg) in arguments.iter().enumerate() { 249 | if let Some(ident) = &arg.ident { 250 | if named_args_positions.insert(ident.clone(), index).is_some() { 251 | return Err(format!("duplicate argument named `{}`", ident).into()); 252 | } 253 | } 254 | } 255 | 256 | let mut arg_indices = Vec::new(); 257 | let mut new_args = Vec::new(); 258 | let mut used_args = vec![false; arguments.len()]; 259 | 260 | let mut process_arg_kind = |arg_kind: &_, spec| { 261 | let index = match *arg_kind { 262 | ArgKind::Positional(index) => { 263 | if index >= arguments.len() { 264 | return Err(format!("invalid positional argument index: {}", index)); 265 | } 266 | 267 | arg_indices.push((index, spec)); 268 | index 269 | } 270 | ArgKind::Named(ref ident) => match named_args_positions.entry(ident.name().to_owned()) { 271 | Entry::Occupied(entry) => { 272 | let index = *entry.get(); 273 | arg_indices.push((index, spec)); 274 | index 275 | } 276 | Entry::Vacant(entry) => { 277 | let new_index = arguments.len() + new_args.len(); 278 | entry.insert(new_index); 279 | arg_indices.push((new_index, spec)); 280 | new_args.push(ident.name()); 281 | new_index 282 | } 283 | }, 284 | }; 285 | 286 | if let Some(used) = used_args.get_mut(index) { 287 | *used = true; 288 | } 289 | 290 | Ok(()) 291 | }; 292 | 293 | for piece in pieces { 294 | match piece { 295 | Piece::StdFmt { arg_kind_position, arg_kind_width, arg_kind_precision } => { 296 | for arg_kind in [Some(arg_kind_position), arg_kind_width, arg_kind_precision].iter().flatten() { 297 | process_arg_kind(arg_kind, None)?; 298 | } 299 | } 300 | Piece::CustomFmt { arg_kind, spec } => process_arg_kind(&arg_kind, Some(spec))?, 301 | } 302 | } 303 | 304 | if let Some((index, (arg, _))) = arguments.iter().zip(&used_args).enumerate().find(|(_, (_, &used))| !used) { 305 | return match &arg.ident { 306 | Some(name) => Err(format!("named argument `{}` not used", name).into()), 307 | None => Err(format!("positional argument {} not used", index).into()), 308 | }; 309 | } 310 | 311 | Ok(ProcessedPieces { arg_indices, new_args }) 312 | } 313 | 314 | #[cfg(test)] 315 | mod test { 316 | use super::*; 317 | 318 | #[test] 319 | fn test_parse_tokens() -> Result<(), Box> { 320 | let s1 = r#" 321 | crate, 322 | [::std::format!], [], 323 | [("format string"), (5==3), (()), (Custom(1f64.abs())), (std::format!("{:?}, {}", (3, 4), 5)), 324 | ((z) = (::std::f64::MAX)), ((r) = (&1 + 4)), ((b) = (2)), ((c) = (Custom(6))), ((e) = ({ g }))] 325 | "#; 326 | 327 | let s2 = r##" 328 | crate, 329 | [::std::format!], [std::io::stdout().lock()], 330 | [(r#"format string"#), (5==3), (()), (Custom(1f64.abs())), (std::format!("{:?}, {}", (3, 4), 5)), 331 | ((z) = (::std::f64::MAX)), ((r) = (&1 + 4)), ((b) = (2)), ((c) = (Custom(6))), ((e) = ({ g }))] 332 | "##; 333 | 334 | let result_format_string = "format string"; 335 | let result_crate_ident = "crate"; 336 | let result_root_macro = "::std::format!".parse::()?.to_string(); 337 | let results_first_arg = [None, Some("std::io::stdout().lock()".parse::()?.to_string())]; 338 | let result_argument_names = [None, None, None, None, Some("z"), Some("r"), Some("b"), Some("c"), Some("e")]; 339 | 340 | let result_argument_exprs = [ 341 | "(5==3)", 342 | "(())", 343 | "(Custom(1f64.abs()))", 344 | r#"(std::format!("{:?}, {}", (3, 4), 5))"#, 345 | "(::std::f64::MAX)", 346 | "(&1 + 4)", 347 | "(2)", 348 | "(Custom(6))", 349 | "({g})", 350 | ]; 351 | 352 | for (s, result_first_arg) in [s1, s2].iter().zip(&results_first_arg) { 353 | let (format_string, parsed_input) = parse_tokens(s.parse()?).unwrap(); 354 | 355 | assert_eq!(format_string, result_format_string); 356 | assert_eq!(parsed_input.crate_ident.to_string(), result_crate_ident); 357 | assert_eq!(parsed_input.root_macro.to_string(), result_root_macro); 358 | assert_eq!(parsed_input.first_arg.map(|x| x.to_string()), *result_first_arg); 359 | 360 | for ((arg, &result_name), &result_expr) in parsed_input.arguments.iter().zip(&result_argument_names).zip(&result_argument_exprs) { 361 | assert_eq!(arg.ident.as_ref().map(|x| x.to_string()), result_name.map(|x| x.to_string())); 362 | assert_eq!(arg.expr.to_string(), result_expr.parse::()?.to_string()); 363 | } 364 | } 365 | 366 | let err = parse_tokens("crate, [::std::format!], [], [(42)]".parse()?).unwrap_err(); 367 | assert!(err.to_string().starts_with("compile_error")); 368 | assert_ne!(err.into_iter().last().unwrap().to_string(), "(\"invalid tokens\")"); 369 | 370 | let err = parse_tokens(TokenStream::new()).unwrap_err(); 371 | assert!(err.to_string().starts_with("compile_error")); 372 | assert_eq!(err.into_iter().last().unwrap().to_string(), "(\"invalid tokens\")"); 373 | 374 | Ok(()) 375 | } 376 | 377 | #[test] 378 | fn test_process_fmt() -> Result<(), Error> { 379 | #[rustfmt::skip] 380 | let data = [ 381 | ("{ :}", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime("") }), 382 | ("{ : \t\r\n }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime("") }), 383 | ("{ :\u{2000} }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime("") }), 384 | ("{ : : : }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime(" : :") }), 385 | ("{ : <: :> }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime(" <: :>") }), 386 | ("{ : éà }" , "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime(" éà") }), 387 | ("{ : <éà> }" , "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime(" <éà>") }), 388 | ("{3 :%a }", "{0}", 0, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(3), spec: Spec::CompileTime("%a") }), 389 | ("{éà :%a}", "{0}", 0, 1, Piece::CustomFmt { arg_kind: ArgKind::Named(Id::new("éà")?), spec: Spec::CompileTime("%a") }), 390 | ("{éà :<<<>>%a><}", "{0}", 0, 1, Piece::CustomFmt { arg_kind: ArgKind::Named(Id::new("éà")?), spec: Spec::CompileTime("<<<>>%a><") }), 391 | ("{ :<>}", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::Runtime("") }), 392 | ("{ :<> \t\r\n }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::Runtime("") }), 393 | ("{ :<>\u{2000} }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::Runtime("") }), 394 | ("{ :< : :> }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::Runtime(" : :") }), 395 | ("{ :<%a> }", "{0}", 1, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::Runtime("%a") }), 396 | ("{3 :<%a> }", "{0}", 0, 1, Piece::CustomFmt { arg_kind: ArgKind::Positional(3), spec: Spec::Runtime("%a") }), 397 | ("{éà :<%a>}", "{0}", 0, 1, Piece::CustomFmt { arg_kind: ArgKind::Named(Id::new("éà")?), spec: Spec::Runtime("%a") }), 398 | ("{éà :<<<>>%a>}", "{0}", 0, 1, Piece::CustomFmt { arg_kind: ArgKind::Named(Id::new("éà")?), spec: Spec::Runtime("<<>>%a") }), 399 | ("{}", "{0}", 1, 1, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: None, arg_kind_precision: None }), 400 | ("{:?}", "{0:?}", 1, 1, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: None, arg_kind_precision: None }), 401 | ("{3:? }", "{0:?}", 0, 1, Piece::StdFmt { arg_kind_position: ArgKind::Positional(3), arg_kind_width: None, arg_kind_precision: None }), 402 | ("{éà}", "{0}", 0, 1, Piece::StdFmt { arg_kind_position: ArgKind::Named(Id::new("éà")?), arg_kind_width: None, arg_kind_precision: None }), 403 | ("{: ^+#03.6? }", "{0: ^+#03.6?}", 1, 1, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: None, arg_kind_precision: None }), 404 | ("{: ^+#0a$.6? }", "{0: ^+#01$.6?}", 1, 2, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: Some(ArgKind::Named(Id::new("a")?)), arg_kind_precision: None }), 405 | ("{: ^+#03.6$? }", "{0: ^+#03.1$?}", 1, 2, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: None, arg_kind_precision: Some(ArgKind::Positional(6)) }), 406 | ("{: ^+#03$.d$? }", "{0: ^+#01$.2$?}", 1, 3, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: Some(ArgKind::Positional(3)), arg_kind_precision: Some(ArgKind::Named(Id::new("d")?)) }), 407 | ("{: ^+#0z$.*? }", "{0: ^+#01$.2$?}", 2, 3, Piece::StdFmt { arg_kind_position: ArgKind::Positional(1), arg_kind_width: Some(ArgKind::Named(Id::new("z")?)), arg_kind_precision: Some(ArgKind::Positional(0)) }), 408 | ("{2: ^+#03$.*? }", "{0: ^+#01$.2$?}", 1, 3, Piece::StdFmt { arg_kind_position: ArgKind::Positional(2), arg_kind_width: Some(ArgKind::Positional(3)), arg_kind_precision: Some(ArgKind::Positional(0)) }), 409 | ("{:1$? }", "{0:1$?}", 1, 2, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: Some(ArgKind::Positional(1)), arg_kind_precision: None }), 410 | ("{:.2$? }", "{0:.1$?}", 1, 2, Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: None, arg_kind_precision: Some(ArgKind::Positional(2)) }), 411 | ("{:.*? }", "{0:.1$?}", 2, 2, Piece::StdFmt { arg_kind_position: ArgKind::Positional(1), arg_kind_width: None, arg_kind_precision: Some(ArgKind::Positional(0)) }), 412 | ("{a:.*? }", "{0:.1$?}", 1, 2, Piece::StdFmt { arg_kind_position: ArgKind::Named(Id::new("a")?), arg_kind_width: None, arg_kind_precision: Some(ArgKind::Positional(0)) }), 413 | ]; 414 | 415 | for &(fmt, result_new_format_string, result_current_positional_index, result_new_current_index, ref result_piece) in &data { 416 | let mut new_format_string = String::new(); 417 | let mut current_positional_index = 0; 418 | let mut new_current_index = 0; 419 | 420 | let piece = process_fmt(fmt, &mut current_positional_index, &mut new_format_string, &mut new_current_index)?; 421 | 422 | assert_eq!(new_format_string, result_new_format_string); 423 | assert_eq!(current_positional_index, result_current_positional_index); 424 | assert_eq!(new_current_index, result_new_current_index); 425 | assert_eq!(piece, *result_piece); 426 | } 427 | 428 | assert_eq!(process_fmt("{: ", &mut 0, &mut String::new(), &mut 0).unwrap_err(), "invalid format string"); 429 | assert_eq!(process_fmt("{0éà0 :%a}", &mut 0, &mut String::new(), &mut 0).unwrap_err(), "invalid format string"); 430 | assert_eq!(process_fmt("{0éà0}", &mut 0, &mut String::new(), &mut 0).unwrap_err(), "invalid format string"); 431 | assert_eq!(process_fmt("{0:.}", &mut 0, &mut String::new(), &mut 0).unwrap_err(), "invalid count in format string"); 432 | assert_eq!(process_fmt("{_:?}", &mut 0, &mut String::new(), &mut 0).unwrap_err(), "invalid argument: argument name cannot be a single underscore"); 433 | 434 | Ok(()) 435 | } 436 | 437 | #[test] 438 | fn test_parse_format_string() -> Result<(), Error> { 439 | let format_string = "aaaa }} {{}}{} {{{{ \" {:#.*} #{h :} {e \u{3A}3xxx\u{47}xxxxxxx }, {:?}, { :}, {:?}, {},,{}, {8 :<>}"; 440 | 441 | let result_new_format_string = "aaaa }} {{}}{0} {{{{ \" {1:#.2$} #{3} {4}, {5:?}, {6}, {7:?}, {8},,{9}, {10}"; 442 | 443 | let result_pieces = [ 444 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(0), arg_kind_width: None, arg_kind_precision: None }, 445 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(2), arg_kind_width: None, arg_kind_precision: Some(ArgKind::Positional(1)) }, 446 | Piece::CustomFmt { arg_kind: ArgKind::Named(Id("h")), spec: Spec::Runtime("z") }, 447 | Piece::CustomFmt { arg_kind: ArgKind::Named(Id("e")), spec: Spec::CompileTime("3xxxGxxxxxxx") }, 448 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(3), arg_kind_width: None, arg_kind_precision: None }, 449 | Piece::CustomFmt { arg_kind: ArgKind::Positional(4), spec: Spec::CompileTime("") }, 450 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(5), arg_kind_width: None, arg_kind_precision: None }, 451 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(6), arg_kind_width: None, arg_kind_precision: None }, 452 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(7), arg_kind_width: None, arg_kind_precision: None }, 453 | Piece::CustomFmt { arg_kind: ArgKind::Positional(8), spec: Spec::Runtime("") }, 454 | ]; 455 | 456 | let (new_format_string, pieces) = parse_format_string(format_string)?; 457 | 458 | assert_eq!(new_format_string, result_new_format_string); 459 | assert_eq!(pieces, result_pieces); 460 | 461 | Ok(()) 462 | } 463 | 464 | #[test] 465 | fn test_process_pieces() -> Result<(), Error> { 466 | let create_argument = |name: Option<&str>| { 467 | let expr = Group::new(Delimiter::Parenthesis, TokenStream::new()); 468 | Argument { ident: name.map(|x| x.to_owned()), expr } 469 | }; 470 | 471 | let pieces = vec![ 472 | Piece::StdFmt { arg_kind_position: ArgKind::Named(Id::new("h")?), arg_kind_width: None, arg_kind_precision: None }, 473 | Piece::CustomFmt { arg_kind: ArgKind::Named(Id::new("h")?), spec: Spec::CompileTime("%z") }, 474 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(1), arg_kind_width: None, arg_kind_precision: None }, 475 | Piece::StdFmt { arg_kind_position: ArgKind::Named(Id::new("a")?), arg_kind_width: None, arg_kind_precision: None }, 476 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(3), arg_kind_width: None, arg_kind_precision: None }, 477 | Piece::StdFmt { arg_kind_position: ArgKind::Named(Id::new("b")?), arg_kind_width: None, arg_kind_precision: None }, 478 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(1), arg_kind_width: None, arg_kind_precision: Some(ArgKind::Positional(0)) }, 479 | Piece::StdFmt { arg_kind_position: ArgKind::Positional(3), arg_kind_width: Some(ArgKind::Named(Id::new("g")?)), arg_kind_precision: None }, 480 | ]; 481 | 482 | let arguments = [create_argument(None), create_argument(Some("a")), create_argument(Some("b")), create_argument(Some("c"))]; 483 | 484 | let result_arg_indices = 485 | [(4, None), (4, Some(Spec::CompileTime("%z"))), (1, None), (1, None), (3, None), (2, None), (1, None), (0, None), (3, None), (5, None)]; 486 | 487 | let result_new_args = ["h", "g"]; 488 | 489 | let processed_pieces = process_pieces(pieces, &arguments)?; 490 | assert_eq!(processed_pieces.arg_indices, result_arg_indices); 491 | assert_eq!(processed_pieces.new_args, result_new_args); 492 | 493 | assert_eq!(process_pieces(vec![], &[create_argument(Some("a")), create_argument(Some("a"))]).unwrap_err(), "duplicate argument named `a`"); 494 | assert_eq!(process_pieces(vec![], &[create_argument(None)]).unwrap_err(), "positional argument 0 not used"); 495 | assert_eq!(process_pieces(vec![], &[create_argument(Some("a"))]).unwrap_err(), "named argument `a` not used"); 496 | 497 | assert_eq!( 498 | process_pieces(vec![], &[create_argument(Some("é")), create_argument(None)]).unwrap_err(), 499 | "positional arguments cannot follow named arguments" 500 | ); 501 | 502 | assert_eq!( 503 | process_pieces(vec![Piece::CustomFmt { arg_kind: ArgKind::Positional(0), spec: Spec::CompileTime("") }], &[]).unwrap_err(), 504 | "invalid positional argument index: 0" 505 | ); 506 | 507 | Ok(()) 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /custom-format-macros/src/fmt/utils.rs: -------------------------------------------------------------------------------- 1 | //! Some useful types. 2 | 3 | use std::str::Chars; 4 | 5 | /// A `StrCursor` contains an iterator over the [char]s of a string slice. 6 | #[derive(Debug, Clone)] 7 | pub struct StrCursor<'a> { 8 | /// Iterator of chars representing the remaining data to be read 9 | chars: Chars<'a>, 10 | } 11 | 12 | impl<'a> StrCursor<'a> { 13 | /// Construct a new `StrCursor` from remaining data 14 | pub fn new(input: &'a str) -> Self { 15 | Self { chars: input.chars() } 16 | } 17 | 18 | /// Returns remaining data 19 | pub fn remaining(&self) -> &'a str { 20 | self.chars.as_str() 21 | } 22 | 23 | /// Returns the next char 24 | pub fn next(&mut self) -> Option { 25 | self.chars.next() 26 | } 27 | 28 | /// Read chars as long as the provided predicate is true 29 | pub fn read_while bool>(&mut self, f: F) -> &'a str { 30 | let remaining = self.chars.as_str(); 31 | 32 | loop { 33 | let old_chars = self.chars.clone(); 34 | 35 | match self.chars.next() { 36 | None => return remaining, 37 | Some(c) => { 38 | if !f(c) { 39 | self.chars = old_chars; 40 | return &remaining[..remaining.len() - self.chars.as_str().len()]; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | /// Read chars until the provided predicate is true 48 | pub fn read_until bool>(&mut self, f: F) -> &'a str { 49 | self.read_while(|x| !f(x)) 50 | } 51 | 52 | /// Read chars until and including the first char for which the provided predicate is true 53 | pub fn read_until_included bool>(&mut self, f: F) -> &'a str { 54 | let remaining = self.chars.as_str(); 55 | self.chars.position(f); 56 | &remaining[..remaining.len() - self.chars.as_str().len()] 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod test { 62 | use super::*; 63 | 64 | #[test] 65 | fn test_remaining() { 66 | let mut cursor = StrCursor::new("©⓪ßéèç0€"); 67 | assert_eq!(cursor.next(), Some('©')); 68 | assert_eq!(cursor.remaining(), "⓪ßéèç0€"); 69 | } 70 | 71 | #[test] 72 | fn test_read_while() { 73 | let mut cursor = StrCursor::new("©⓪ßéèç0€"); 74 | assert_eq!(cursor.read_while(|c| c != 'ß'), "©⓪"); 75 | assert_eq!(cursor.read_while(|c| c != 'ç'), "ßéè"); 76 | assert_eq!(cursor.read_while(|c| c != ' '), "ç0€"); 77 | } 78 | 79 | #[test] 80 | fn test_read_until() { 81 | let mut cursor = StrCursor::new("©⓪ßéèç0€"); 82 | assert_eq!(cursor.read_until(|c| c == 'ß'), "©⓪"); 83 | assert_eq!(cursor.read_until(|c| c == 'ç'), "ßéè"); 84 | assert_eq!(cursor.read_until(|c| c == ' '), "ç0€"); 85 | } 86 | 87 | #[test] 88 | fn test_read_until_included() { 89 | let mut cursor = StrCursor::new("©⓪ßéèç0€"); 90 | assert_eq!(cursor.read_until_included(|c| c == 'ß'), "©⓪ß"); 91 | assert_eq!(cursor.read_until_included(|c| c == 'ç'), "éèç"); 92 | assert_eq!(cursor.read_until_included(|c| c == ' '), "0€"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /custom-format-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![deny(missing_docs)] 3 | 4 | //! This crate provides procedural macros used for the `custom-format` crate. 5 | 6 | mod fmt; 7 | 8 | use proc_macro::TokenStream; 9 | 10 | /// Parse custom format specifiers in format string and write output tokens. 11 | /// 12 | /// This is an internal unstable macro and should not be used directly. 13 | #[proc_macro] 14 | #[allow(clippy::useless_conversion)] 15 | pub fn fmt(input: TokenStream) -> TokenStream { 16 | fmt::fmt(input.into()).into() 17 | } 18 | -------------------------------------------------------------------------------- /custom-format-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom-format-tests" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | custom-format = { path = "..", default-features = false } 9 | 10 | [features] 11 | compile-time = ["custom-format/compile-time"] 12 | runtime = ["custom-format/runtime"] 13 | default = ["compile-time", "runtime"] 14 | -------------------------------------------------------------------------------- /custom-format-tests/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests; 3 | 4 | fn main() { 5 | println!("Test crate.") 6 | } 7 | -------------------------------------------------------------------------------- /custom-format-tests/src/tests.rs: -------------------------------------------------------------------------------- 1 | use custom_format as cfmt; 2 | 3 | #[test] 4 | fn test_format_args() { 5 | println!("{}", cfmt::format_args!("string")); 6 | println!("{}", cfmt::format_args!("{}", "string")); 7 | cfmt::println!("{}", format_args!("string")); 8 | cfmt::println!("{}", format_args!("{}", "string")); 9 | cfmt::println!("{}", cfmt::format_args!("string")); 10 | cfmt::println!("{}", cfmt::format_args!("{}", "string")); 11 | } 12 | 13 | #[test] 14 | fn test_print() { 15 | cfmt::print!("string\n"); 16 | cfmt::print!("{}", "string\n"); 17 | cfmt::println!("string"); 18 | cfmt::println!("{}", "string"); 19 | cfmt::eprint!("string\n"); 20 | cfmt::eprint!("{}", "string\n"); 21 | cfmt::eprintln!("string"); 22 | cfmt::eprintln!("{}", "string"); 23 | } 24 | 25 | #[test] 26 | fn test_write() { 27 | use std::io::Write; 28 | 29 | let mut v = Vec::new(); 30 | let _ = cfmt::write!(v, "string\n"); 31 | let _ = cfmt::write!(v, "{}", "string\n"); 32 | let _ = cfmt::writeln!(v, "string"); 33 | let _ = cfmt::writeln!(v, "{}", "string"); 34 | } 35 | 36 | #[test] 37 | #[should_panic(expected = "string")] 38 | fn test_panic_1() { 39 | cfmt::panic!("string"); 40 | } 41 | 42 | #[test] 43 | #[should_panic(expected = "string")] 44 | fn test_panic_2() { 45 | cfmt::panic!("{}", "string"); 46 | } 47 | 48 | #[test] 49 | fn test_no_format_string() { 50 | cfmt::println!(); 51 | cfmt::eprintln!(); 52 | } 53 | 54 | #[test] 55 | fn test_literal_format_string() { 56 | assert_eq!(cfmt::format!("string"), "string"); 57 | } 58 | 59 | #[test] 60 | fn test_std_fmt() { 61 | assert_eq!(cfmt::format!("Hello"), "Hello"); 62 | assert_eq!(cfmt::format!("Hello, {}!", "world"), "Hello, world!"); 63 | assert_eq!(cfmt::format!("The number is {}", 1), "The number is 1"); 64 | assert_eq!(cfmt::format!("{:?}", (3, 4)), "(3, 4)"); 65 | assert_eq!(cfmt::format!("{value}", value = 4), "4"); 66 | let people = "Rustaceans"; 67 | assert_eq!(cfmt::format!("Hello {people}!"), "Hello Rustaceans!"); 68 | assert_eq!(cfmt::format!("{} {}", 1, 2), "1 2"); 69 | assert_eq!(cfmt::format!("{:04}", 42), "0042"); 70 | assert_eq!(cfmt::format!("{:#?}", (100, 200)), "(\n 100,\n 200,\n)"); 71 | assert_eq!(cfmt::format!("{1} {} {0} {}", 1, 2), "2 1 1 2"); 72 | assert_eq!(cfmt::format!("{argument}", argument = "test"), "test"); 73 | assert_eq!(cfmt::format!("{name} {}", 1, name = 2), "2 1"); 74 | assert_eq!(cfmt::format!("{a} {c} {b}", a = "a", b = 'b', c = 3), "a 3 b"); 75 | assert_eq!(cfmt::format!("Hello {:5}!", "x"), "Hello x !"); 76 | assert_eq!(cfmt::format!("Hello {:1$}!", "x", 5), "Hello x !"); 77 | assert_eq!(cfmt::format!("Hello {1:0$}!", 5, "x"), "Hello x !"); 78 | assert_eq!(cfmt::format!("Hello {:width$}!", "x", width = 5), "Hello x !"); 79 | let width = 5; 80 | assert_eq!(cfmt::format!("Hello {:width$}!", "x"), "Hello x !"); 81 | assert_eq!(cfmt::format!("Hello {:<5}!", "x"), "Hello x !"); 82 | assert_eq!(cfmt::format!("Hello {:-<5}!", "x"), "Hello x----!"); 83 | assert_eq!(cfmt::format!("Hello {:^5}!", "x"), "Hello x !"); 84 | assert_eq!(cfmt::format!("Hello {:>5}!", "x"), "Hello x!"); 85 | assert_eq!(cfmt::format!("Hello {:^15}!", cfmt::format!("{:?}", Some("hi"))), "Hello Some(\"hi\") !"); 86 | assert_eq!(cfmt::format!("Hello {:+}!", 5), "Hello +5!"); 87 | assert_eq!(cfmt::format!("{:#x}!", 27), "0x1b!"); 88 | assert_eq!(cfmt::format!("Hello {:05}!", 5), "Hello 00005!"); 89 | assert_eq!(cfmt::format!("Hello {:05}!", -5), "Hello -0005!"); 90 | assert_eq!(cfmt::format!("{:#010x}!", 27), "0x0000001b!"); 91 | assert_eq!(cfmt::format!("Hello {0} is {1:.5}", "x", 0.01), "Hello x is 0.01000"); 92 | assert_eq!(cfmt::format!("Hello {1} is {2:.0$}", 5, "x", 0.01), "Hello x is 0.01000"); 93 | assert_eq!(cfmt::format!("Hello {0} is {2:.1$}", "x", 5, 0.01), "Hello x is 0.01000"); 94 | assert_eq!(cfmt::format!("Hello {} is {:.*}", "x", 5, 0.01), "Hello x is 0.01000"); 95 | assert_eq!(cfmt::format!("Hello {1} is {2:.*}", 5, "x", 0.01), "Hello x is 0.01000"); 96 | assert_eq!(cfmt::format!("Hello {} is {2:.*}", "x", 5, 0.01), "Hello x is 0.01000"); 97 | assert_eq!(cfmt::format!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01), "Hello x is 0.01000"); 98 | assert_eq!(cfmt::format!("{}, `{name:.*}`", "Hello", 3, name = 1234.56), "Hello, `1234.560`"); 99 | assert_eq!(cfmt::format!("{}, `{name:.*}`", "Hello", 3, name = "1234.56"), "Hello, `123`"); 100 | assert_eq!(cfmt::format!("{}, `{name:>8.*}`", "Hello", 3, name = "1234.56"), "Hello, ` 123`"); 101 | assert_eq!(cfmt::format!("Hello {{}}"), "Hello {}"); 102 | assert_eq!(cfmt::format!("{{ Hello"), "{ Hello"); 103 | assert_eq!(cfmt::format!("{: ^+2$.*e}", 5, -0.01, 15), " -1.00000e-2 "); 104 | assert_eq!(cfmt::format!("Hello {::>9.*x? }!", 3, 1.0), "Hello ::::1.000!"); 105 | 106 | assert_eq!(cfmt::format!("{h}, {h}, {1}, {1}, {a}, {a}, {3}, {b}, {:.*}", 3, a = 1f64.abs(), b = &(1 + 4), c = 2, h = 0), "0, 0, 1, 1, 1, 1, 2, 5, 1.000"); 107 | } 108 | 109 | #[cfg(all(feature = "compile-time", feature = "runtime"))] 110 | #[test] 111 | fn test_custom_formatter() { 112 | use core::fmt; 113 | 114 | struct Custom(T); 115 | 116 | impl fmt::Display for Custom { 117 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 118 | write!(f, "{}", self.0) 119 | } 120 | } 121 | 122 | macro_rules! impl_custom_format { 123 | (match spec { $($spec:literal => $func:expr $(,)?)* }) => { 124 | use cfmt::compile_time::{spec, CustomFormat}; 125 | $( 126 | impl CustomFormat<{ spec($spec) }> for Custom { 127 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 128 | ($func as fn(&Self, &mut fmt::Formatter) -> fmt::Result)(self, f) 129 | } 130 | } 131 | )* 132 | }; 133 | } 134 | 135 | impl_custom_format!(match spec { 136 | "" => |this, f| write!(f, "({} with spec '')", this.0), 137 | "3xxGxx" => |this, f| write!(f, "({} with spec '3xxGxx')", this.0), 138 | }); 139 | 140 | impl cfmt::runtime::CustomFormat for Custom { 141 | fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result { 142 | write!(f, "({} with runtime spec '{}')", self.0, spec) 143 | } 144 | } 145 | 146 | let (g, h) = (Custom(0), Custom(0)); 147 | 148 | let result = cfmt::format!( 149 | "aaaa }} {{}}{} {{{{ \" {:#.*} #{h : } {e \u{3A}3xx\u{47}xx }, {:?}, { :}, {:?}, {},,{}, {8 :<>}", 150 | "ok", 151 | 5, 152 | Custom(0.01), 153 | (), 154 | Custom(1f64.abs()), 155 | std::format!("{:?}, {}", (3, 4), 5), 156 | r = &1 + 4, 157 | b = 2, 158 | c = Custom(6), 159 | e = { g }, 160 | ); 161 | 162 | assert_eq!( 163 | result, 164 | "aaaa } {}ok {{ \" 0.01 #(0 with runtime spec 'z') (0 with spec '3xxGxx'), (), (1 with spec ''), \"(3, 4), 5\", 5,,2, (6 with runtime spec '')" 165 | ); 166 | } 167 | 168 | #[cfg(feature = "compile-time")] 169 | #[test] 170 | fn test_spec() { 171 | assert_eq!(cfmt::compile_time::spec(""), 0); 172 | assert_eq!(cfmt::compile_time::spec("AB"), 0x4241); 173 | assert_eq!(cfmt::compile_time::spec("é"), 0xA9C3); 174 | assert_eq!(cfmt::compile_time::spec("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0~"), 0x7E000000000000000000000000000000); 175 | } 176 | 177 | #[cfg(feature = "runtime")] 178 | #[test] 179 | fn test_custom_formatter_runtime() { 180 | use core::fmt; 181 | 182 | struct Custom; 183 | 184 | impl cfmt::runtime::CustomFormat for Custom { 185 | fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result { 186 | write!(f, "{}", spec) 187 | } 188 | } 189 | 190 | assert_eq!(cfmt::format!("{ :}", Custom), "x"); 191 | } 192 | 193 | #[cfg(feature = "runtime")] 194 | #[test] 195 | #[should_panic(expected = "a formatting trait implementation returned an error")] 196 | fn test_custom_formatter_runtime_panic() { 197 | use core::fmt; 198 | 199 | struct Hex(u8); 200 | 201 | impl cfmt::runtime::CustomFormat for Hex { 202 | fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result { 203 | match spec { 204 | "x" => write!(f, "{:#02x}", self.0), 205 | "X" => write!(f, "{:#02X}", self.0), 206 | _ => Err(fmt::Error), 207 | } 208 | } 209 | } 210 | 211 | cfmt::format!("{ :<>}", Hex(0xAB)); 212 | } 213 | -------------------------------------------------------------------------------- /examples/strftime.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(all(feature = "compile-time", feature = "runtime"))] 3 | { 4 | use custom_format as cfmt; 5 | 6 | use core::fmt; 7 | 8 | pub struct DateTime { 9 | year: i32, 10 | month: u8, 11 | month_day: u8, 12 | hour: u8, 13 | minute: u8, 14 | second: u8, 15 | nanoseconds: u32, 16 | } 17 | 18 | macro_rules! impl_custom_format_for_datetime { 19 | (match spec { $($spec:literal => $func:expr $(,)?)* }) => { 20 | use cfmt::compile_time::{spec, CustomFormat}; 21 | $( 22 | impl CustomFormat<{ spec($spec) }> for DateTime { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | ($func as fn(&Self, &mut fmt::Formatter) -> fmt::Result)(self, f) 25 | } 26 | } 27 | )* 28 | }; 29 | } 30 | 31 | // Static format specifiers, checked at compile-time 32 | impl_custom_format_for_datetime!(match spec { 33 | // Year with pad for at least 4 digits 34 | "%Y" => |this, f| write!(f, "{:04}", this.year), 35 | // Year % 100 (00..99) 36 | "%y" => |this, f| write!(f, "{:02}", (this.year % 100).abs()), 37 | // Month of the year, zero-padded (01..12) 38 | "%m" => |this, f| write!(f, "{:02}", this.month), 39 | // Day of the month, zero-padded (01..31) 40 | "%d" => |this, f| write!(f, "{:02}", this.month_day), 41 | // Hour of the day, 24-hour clock, zero-padded (00..23) 42 | "%H" => |this, f| write!(f, "{:02}", this.hour), 43 | // Minute of the hour (00..59) 44 | "%M" => |this, f| write!(f, "{:02}", this.minute), 45 | // Second of the minute (00..60) 46 | "%S" => |this, f| write!(f, "{:02}", this.second), 47 | // Date (%m/%d/%y) 48 | "%D" => { 49 | |this, f| { 50 | let month = cfmt::custom_formatter!("%m", this); 51 | let day = cfmt::custom_formatter!("%d", this); 52 | let year = cfmt::custom_formatter!("%y", this); 53 | write!(f, "{}/{}/{}", month, day, year) 54 | } 55 | } 56 | }); 57 | 58 | // Dynamic format specifiers, checked at runtime 59 | impl cfmt::runtime::CustomFormat for DateTime { 60 | fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result { 61 | let mut chars = spec.chars(); 62 | match (chars.next(), chars.next_back()) { 63 | // Nanoseconds with n digits (%nN) 64 | (Some('%'), Some('N')) => match chars.as_str().parse() { 65 | Ok(n) if n > 0 => { 66 | if n <= 9 { 67 | write!(f, "{:0width$}", self.nanoseconds / 10u32.pow(9 - n as u32), width = n) 68 | } else { 69 | write!(f, "{:09}{:0width$}", self.nanoseconds, 0, width = n - 9) 70 | } 71 | } 72 | _ => Err(fmt::Error), 73 | }, 74 | _ => Err(fmt::Error), 75 | } 76 | } 77 | } 78 | 79 | let dt = DateTime { year: 1836, month: 5, month_day: 18, hour: 23, minute: 45, second: 54, nanoseconds: 123456789 }; 80 | 81 | // Expands to: 82 | // 83 | // match (&("DateTime"), &dt) { 84 | // (arg0, arg1) => ::std::println!( 85 | // "The {0:?} is: {1}-{2}-{3} {4}:{5}:{6}.{7}", 86 | // arg0, 87 | // ::custom_format::custom_formatter!("%Y", arg1), 88 | // ::custom_format::custom_formatter!("%m", arg1), 89 | // ::custom_format::custom_formatter!("%d", arg1), 90 | // ::custom_format::custom_formatter!("%H", arg1), 91 | // ::custom_format::custom_formatter!("%M", arg1), 92 | // ::custom_format::custom_formatter!("%S", arg1), 93 | // ::custom_format::runtime::CustomFormatter::new("%6N", arg1) 94 | // ), 95 | // } 96 | // 97 | // Output: `The "DateTime" is: 1836-05-18 23:45:54.123456` 98 | // 99 | // The custom format specifier is interpreted as a compile-time specifier by default, or as a runtime specifier if it is inside "<>". 100 | cfmt::println!("The {ty:?} is: {dt :%Y}-{dt :%m}-{dt :%d} {dt :%H}:{dt :%M}:{dt :%S}.{dt :<%6N>}", ty = "DateTime"); 101 | 102 | // Compile-time error since "%h" is not a valid format specifier 103 | // cfmt::println!("{0 :%h}", dt); 104 | 105 | // Panic at runtime since "%h" is not a valid format specifier 106 | // cfmt::println!("{0 :<%h>}", dt); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 160 2 | use_small_heuristics = "Max" 3 | -------------------------------------------------------------------------------- /src/compile_time.rs: -------------------------------------------------------------------------------- 1 | //! Provides types associated to compile-time formatting. 2 | 3 | use core::fmt; 4 | 5 | /// Trait for custom formatting with compile-time format checking 6 | pub trait CustomFormat { 7 | /// Formats the value using the given formatter. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ```rust 12 | /// use custom_format as cfmt; 13 | /// use custom_format::compile_time::{spec, CustomFormat}; 14 | /// 15 | /// use core::fmt; 16 | /// 17 | /// #[derive(Debug)] 18 | /// struct Hex(u8); 19 | /// 20 | /// impl CustomFormat<{ spec("x") }> for Hex { 21 | /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | /// write!(f, "{:#02x}", self.0) 23 | /// } 24 | /// } 25 | /// 26 | /// impl CustomFormat<{ spec("X") }> for Hex { 27 | /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | /// write!(f, "{:#02X}", self.0) 29 | /// } 30 | /// } 31 | /// 32 | /// assert_eq!(cfmt::format!("{0:X?}, {0 :x}, {0 :X}", Hex(0xAB)), "Hex(AB), 0xab, 0xAB"); 33 | /// ``` 34 | /// 35 | /// The following statement doesn't compile since `"z"` is not a valid format specifier: 36 | /// 37 | /// ```rust,compile_fail 38 | /// # use custom_format as cfmt; 39 | /// # use custom_format::compile_time::{spec, CustomFormat}; 40 | /// # use core::fmt; 41 | /// # struct Hex(u8); 42 | /// # impl CustomFormat<{ cfmt::spec("x") }> for Hex { 43 | /// # fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 44 | /// # write!(f, "{:#02x}", self.0) 45 | /// # } 46 | /// # } 47 | /// # impl CustomFormat<{ cfmt::spec("X") }> for Hex { 48 | /// # fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 | /// # write!(f, "{:#02X}", self.0) 50 | /// # } 51 | /// # } 52 | /// cfmt::println!("{ :z}", Hex(0)); 53 | /// ``` 54 | /// 55 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result; 56 | } 57 | 58 | /// Wrapper for custom formatting via its [`Display`](core::fmt::Display) trait. 59 | /// 60 | /// The format specifier is a const-generic parameter and is part of the type. 61 | /// 62 | #[derive(Debug, Clone)] 63 | pub struct CustomFormatter<'a, T, const SPEC: u128> { 64 | /// Value to format 65 | value: &'a T, 66 | } 67 | 68 | impl<'a, T, const SPEC: u128> CustomFormatter<'a, T, SPEC> { 69 | /// Construct a new [`CustomFormatter`] value 70 | pub fn new(value: &'a T) -> Self { 71 | Self { value } 72 | } 73 | } 74 | 75 | /// Helper macro for constructing a new [`compile_time::CustomFormatter`](CustomFormatter) value from a format specifier 76 | #[macro_export] 77 | macro_rules! custom_formatter { 78 | ($spec:literal, $value:expr) => {{ 79 | $crate::compile_time::CustomFormatter::<_, { $crate::compile_time::spec($spec) }>::new($value) 80 | }}; 81 | } 82 | pub use custom_formatter; 83 | 84 | impl, const SPEC: u128> fmt::Display for CustomFormatter<'_, T, SPEC> { 85 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 86 | CustomFormat::fmt(self.value, f) 87 | } 88 | } 89 | 90 | /// Convert a format specifier to a [`u128`], used as a const-generic parameter 91 | pub const fn spec(s: &str) -> u128 { 92 | let bytes = s.as_bytes(); 93 | let len = s.len(); 94 | 95 | if len > 16 { 96 | #[allow(unconditional_panic, clippy::out_of_bounds_indexing)] 97 | let _ = ["format specifier is limited to 16 bytes"][usize::MAX]; 98 | } 99 | 100 | let mut result = [0u8; 16]; 101 | 102 | let mut i = 0; 103 | while i < len { 104 | result[i] = bytes[i]; 105 | i += 1; 106 | } 107 | 108 | u128::from_le_bytes(result) 109 | } 110 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![forbid(unsafe_code)] 3 | #![deny(missing_docs)] 4 | #![cfg_attr(docsrs, feature(doc_cfg))] 5 | 6 | //! This crate extends the standard formatting syntax with custom format specifiers, by providing custom formatting macros. 7 | //! 8 | //! It uses ` :` (a space and a colon) as a separator before the format specifier, which is not a syntax currently accepted and allows supporting standard specifiers in addition to custom specifiers. 9 | //! It also supports [format args capture](https://blog.rust-lang.org/2022/01/13/Rust-1.58.0.html#captured-identifiers-in-format-strings) 10 | //! even on older versions of Rust, since it manually adds the named parameter if missing. 11 | //! 12 | //! This library comes in two flavors, corresponding to the following features: 13 | //! 14 | //! - `compile-time` (*enabled by default*) 15 | //! 16 | //! The set of possible custom format specifiers is defined at compilation, so invalid specifiers can be checked at compile-time. 17 | //! This allows the library to have the same performance as when using the standard library formatting traits. 18 | //! See the [`compile_time::CustomFormat`] trait. 19 | //! 20 | //! - `runtime` (*enabled by default*) 21 | //! 22 | //! The formatting method dynamically checks the format specifier at runtime for each invocation. 23 | //! This is a slower version, but has a lower MSRV for greater compatibility. 24 | //! See the [`runtime::CustomFormat`] trait. 25 | 26 | #[cfg(feature = "compile-time")] 27 | #[cfg_attr(docsrs, doc(cfg(feature = "compile-time")))] 28 | pub mod compile_time; 29 | 30 | #[cfg(feature = "runtime")] 31 | #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] 32 | pub mod runtime; 33 | 34 | #[doc(hidden)] 35 | pub use custom_format_macros; 36 | 37 | #[doc(hidden)] 38 | #[macro_export] 39 | macro_rules! parse_args { 40 | ([$($macro:tt)*], [$($first_arg:expr)?], [$($result:expr),*], $id:ident = $expr:expr, $($arg:tt)*) => {{ 41 | $crate::parse_args!([$($macro)*], [$($first_arg)?], [$($result,)* ($id) = $expr], $($arg)*) 42 | }}; 43 | ([$($macro:tt)*], [$($first_arg:expr)?], [$($result:expr),*], $expr:expr, $($arg:tt)*) => {{ 44 | $crate::parse_args!([$($macro)*], [$($first_arg)?], [$($result,)* $expr], $($arg)*) 45 | }}; 46 | ([$($macro:tt)*], [$($first_arg:expr)?], [$($result:expr),*], $(,)?) => {{ 47 | $crate::custom_format_macros::fmt!($crate, [$($macro)*], [$($first_arg)?], [$($result),*]) 48 | }}; 49 | } 50 | 51 | #[doc(hidden)] 52 | #[macro_export] 53 | macro_rules! fmt_inner { 54 | ([$($macro:tt)*], [$($first_arg:expr)?], ) => {{ 55 | compile_error!("requires at least a format string argument") 56 | }}; 57 | ([$($macro:tt)*], [$($first_arg:expr)?], $fmt:literal) => {{ 58 | $crate::custom_format_macros::fmt!($crate, [$($macro)*], [$($first_arg)?], [$fmt]) 59 | }}; 60 | ([$($macro:tt)*], [$($first_arg:expr)?], $fmt:literal, $($arg:tt)*) => {{ 61 | $crate::parse_args!([$($macro)*], [$($first_arg)?], [$fmt], $($arg)*,) 62 | }}; 63 | } 64 | 65 | /// Constructs parameters for the other string-formatting macros. 66 | /// 67 | /// ## Important note 68 | /// 69 | /// The other macros in this crate use an inner `match` to avoid reevaluating the input arguments several times. 70 | /// 71 | /// For example, the following `println!` call: 72 | /// 73 | /// ```rust 74 | /// use custom_format as cfmt; 75 | /// use core::fmt; 76 | /// 77 | /// #[derive(Debug)] 78 | /// struct Hex(u8); 79 | /// 80 | /// impl cfmt::runtime::CustomFormat for Hex { 81 | /// fn fmt(&self, f: &mut fmt::Formatter, _: &str) -> fmt::Result { 82 | /// write!(f, "{:#02x}", self.0) 83 | /// } 84 | /// } 85 | /// 86 | /// fn call() -> Hex { 87 | /// Hex(42) 88 | /// } 89 | /// 90 | /// cfmt::println!("{0:?}, {res :}", res = call()); 91 | /// ``` 92 | /// 93 | /// is expanded to: 94 | /// 95 | /// ```rust 96 | /// # use custom_format as cfmt; 97 | /// # use core::fmt; 98 | /// # #[derive(Debug)] 99 | /// # struct Hex(u8); 100 | /// # impl cfmt::runtime::CustomFormat for Hex { 101 | /// # fn fmt(&self, f: &mut fmt::Formatter, _: &str) -> fmt::Result { 102 | /// # write!(f, "{:#02x}", self.0) 103 | /// # } 104 | /// # } 105 | /// # fn call() -> Hex { Hex(42) } 106 | /// match (&(call())) { 107 | /// (arg0) => ::std::println!("{0:?}, {1}", arg0, cfmt::runtime::CustomFormatter::new("x", arg0)), 108 | /// } 109 | /// ``` 110 | /// 111 | /// This method doesn't work with the `format_args!` macro, since it returns a value of type [`core::fmt::Arguments`] 112 | /// which borrows the temporary values of the `match`. Since these temporary values are dropped before returning, 113 | /// the return value cannot be used at all if the format string contains format specifiers. 114 | /// 115 | /// For this reason, the `format_args!` macro is expanded in another way. The following call: 116 | /// 117 | /// ```rust 118 | /// # use custom_format as cfmt; 119 | /// # use core::fmt; 120 | /// # #[derive(Debug)] 121 | /// # struct Hex(u8); 122 | /// # impl cfmt::runtime::CustomFormat for Hex { 123 | /// # fn fmt(&self, f: &mut fmt::Formatter, _: &str) -> fmt::Result { 124 | /// # write!(f, "{:#02x}", self.0) 125 | /// # } 126 | /// # } 127 | /// # fn call() -> Hex { Hex(42) } 128 | /// println!("{}", cfmt::format_args!("{0:?}, {res :}", res = call())); 129 | /// ``` 130 | /// 131 | /// must be expanded to: 132 | /// 133 | /// ```rust 134 | /// # use custom_format as cfmt; 135 | /// # use core::fmt; 136 | /// # #[derive(Debug)] 137 | /// # struct Hex(u8); 138 | /// # impl cfmt::runtime::CustomFormat for Hex { 139 | /// # fn fmt(&self, f: &mut fmt::Formatter, _: &str) -> fmt::Result { 140 | /// # write!(f, "{:#02x}", self.0) 141 | /// # } 142 | /// # } 143 | /// # fn call() -> Hex { Hex(42) } 144 | /// println!("{}", ::core::format_args!("{0:?}, {1}", &(call()), cfmt::runtime::CustomFormatter::new("x", &(call())))); 145 | /// ``` 146 | /// 147 | /// which reevaluates the input arguments if they are used several times in the format string. 148 | /// 149 | /// To avoid unnecessary reevaluations, we can store the expression result in a variable beforehand: 150 | /// 151 | /// ```rust 152 | /// # use custom_format as cfmt; 153 | /// # use core::fmt; 154 | /// # #[derive(Debug)] 155 | /// # struct Hex(u8); 156 | /// # impl cfmt::runtime::CustomFormat for Hex { 157 | /// # fn fmt(&self, f: &mut fmt::Formatter, _: &str) -> fmt::Result { 158 | /// # write!(f, "{:#02x}", self.0) 159 | /// # } 160 | /// # } 161 | /// # fn call() -> Hex { Hex(42) } 162 | /// let res = call(); 163 | /// println!("{}", cfmt::format_args!("{res:?}, {res :}")); 164 | /// ``` 165 | /// 166 | /// is expanded to: 167 | /// 168 | /// ```rust 169 | /// # use custom_format as cfmt; 170 | /// # use core::fmt; 171 | /// # #[derive(Debug)] 172 | /// # struct Hex(u8); 173 | /// # impl cfmt::runtime::CustomFormat for Hex { 174 | /// # fn fmt(&self, f: &mut fmt::Formatter, _: &str) -> fmt::Result { 175 | /// # write!(f, "{:#02x}", self.0) 176 | /// # } 177 | /// # } 178 | /// # fn call() -> Hex { Hex(42) } 179 | /// # let res = call(); 180 | /// println!("{}", ::core::format_args!("{0:?}, {1}", &res, cfmt::runtime::CustomFormatter::new("x", &res))) 181 | /// ``` 182 | #[macro_export] 183 | macro_rules! format_args { 184 | ($($arg:tt)*) => {{ 185 | $crate::fmt_inner!([::core::format_args!], [], $($arg)*) 186 | }}; 187 | } 188 | 189 | /// Creates a `String` using interpolation of runtime expressions 190 | #[macro_export] 191 | macro_rules! format { 192 | ($($arg:tt)*) => {{ 193 | $crate::fmt_inner!([::std::format!], [], $($arg)*) 194 | }}; 195 | } 196 | 197 | /// Prints to the standard output 198 | #[macro_export] 199 | macro_rules! print { 200 | ($($arg:tt)*) => {{ 201 | $crate::fmt_inner!([::std::print!], [], $($arg)*) 202 | }}; 203 | } 204 | 205 | /// Prints to the standard output, with a newline 206 | #[macro_export] 207 | macro_rules! println { 208 | () => {{ 209 | ::std::println!() 210 | }}; 211 | ($($arg:tt)*) => {{ 212 | $crate::fmt_inner!([::std::println!], [], $($arg)*) 213 | }}; 214 | } 215 | 216 | /// Prints to the standard error 217 | #[macro_export] 218 | macro_rules! eprint { 219 | ($($arg:tt)*) => {{ 220 | $crate::fmt_inner!([::std::eprint!], [], $($arg)*) 221 | }}; 222 | } 223 | 224 | /// Prints to the standard error, with a newline 225 | #[macro_export] 226 | macro_rules! eprintln { 227 | () => {{ 228 | ::std::eprintln!() 229 | }}; 230 | ($($arg:tt)*) => {{ 231 | $crate::fmt_inner!([::std::eprintln!], [], $($arg)*) 232 | }}; 233 | } 234 | 235 | /// Writes formatted data into a buffer 236 | #[macro_export] 237 | macro_rules! write { 238 | ($dst:expr, $($arg:tt)*) => {{ 239 | $crate::fmt_inner!([::core::write!], [$dst], $($arg)*) 240 | }}; 241 | } 242 | 243 | /// Write formatted data into a buffer, with a newline appended 244 | #[macro_export] 245 | macro_rules! writeln { 246 | ($dst:expr) => {{ 247 | ::core::writeln!($dst) 248 | }}; 249 | ($dst:expr, $($arg:tt)*) => {{ 250 | $crate::fmt_inner!([::core::writeln!], [$dst], $($arg)*) 251 | }}; 252 | } 253 | 254 | /// Panics the current thread 255 | #[macro_export] 256 | macro_rules! panic { 257 | () => {{ 258 | ::core::panic!() 259 | }}; 260 | ($($arg:tt)*) => {{ 261 | $crate::fmt_inner!([::core::panic!], [], $($arg)*) 262 | }}; 263 | } 264 | -------------------------------------------------------------------------------- /src/runtime.rs: -------------------------------------------------------------------------------- 1 | //! Provides types associated to runtime formatting. 2 | 3 | use core::fmt; 4 | 5 | /// Trait for custom formatting with runtime format checking 6 | pub trait CustomFormat { 7 | /// Formats the value using the given formatter. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ```rust 12 | /// use custom_format as cfmt; 13 | /// 14 | /// use core::fmt; 15 | /// 16 | /// #[derive(Debug)] 17 | /// struct Hex(u8); 18 | /// 19 | /// impl cfmt::runtime::CustomFormat for Hex { 20 | /// fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result { 21 | /// match spec { 22 | /// "x" => write!(f, "{:#02x}", self.0), 23 | /// "X" => write!(f, "{:#02X}", self.0), 24 | /// _ => Err(fmt::Error), 25 | /// } 26 | /// } 27 | /// } 28 | /// 29 | /// // The custom format specifier is interpreted as a runtime specifier when it is inside "<>" 30 | /// assert_eq!(cfmt::format!("{0:X?}, {0 :}, {0 :}", Hex(0xAB)), "Hex(AB), 0xab, 0xAB"); 31 | /// ``` 32 | /// 33 | /// The following statement panics at runtime since `"z"` is not a valid format specifier: 34 | /// 35 | /// ```rust,should_panic 36 | /// # use custom_format as cfmt; 37 | /// # use core::fmt; 38 | /// # struct Hex(u8); 39 | /// # impl cfmt::runtime::CustomFormat for Hex { 40 | /// # fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result { 41 | /// # match spec { 42 | /// # "x" => write!(f, "{:#02x}", self.0), 43 | /// # "X" => write!(f, "{:#02X}", self.0), 44 | /// # _ => Err(fmt::Error), 45 | /// # } 46 | /// # } 47 | /// # } 48 | /// cfmt::println!("{ :}", Hex(0)); 49 | /// ``` 50 | /// 51 | fn fmt(&self, f: &mut fmt::Formatter, spec: &str) -> fmt::Result; 52 | } 53 | 54 | /// Wrapper for custom formatting via its [`Display`](core::fmt::Display) trait 55 | #[derive(Debug, Clone)] 56 | pub struct CustomFormatter<'a, T> { 57 | /// Format specifier 58 | spec: &'static str, 59 | /// Value to format 60 | value: &'a T, 61 | } 62 | 63 | impl<'a, T> CustomFormatter<'a, T> { 64 | /// Construct a new [`CustomFormatter`] value 65 | pub fn new(spec: &'static str, value: &'a T) -> Self { 66 | Self { spec, value } 67 | } 68 | } 69 | 70 | impl fmt::Display for CustomFormatter<'_, T> { 71 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 72 | CustomFormat::fmt(self.value, f, self.spec) 73 | } 74 | } 75 | --------------------------------------------------------------------------------