├── .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 | [](https://crates.io/crates/custom-format)
4 | 
5 | [](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