2 unstable releases
Uses new Rust 2024
new 0.2.0 | May 5, 2025 |
---|---|
0.1.0 | May 5, 2025 |
#459 in Value formatting
31KB
299 lines
format-like

A macro for creating format-like macros
Have you ever wanted to emulate the functionality of the format!
family of macros, but with an output that is not a String
or
something built from a String
?
No?
Well, still, this might still be interesting for you.
format-like
aims to let you decide how to interpret what is
inside {}
pairs, instead of calling something like
std::fmt::Display::fmt(&value)
.
Additionaly, it lets you create 3 other types of bracket pairs:
()
, []
and <>
, so you can interpret things in even more
ways!
Here’s how it works:
use format_like::format_like;
struct CommentedString(String, Vec<(usize, String)>);
let comment = "there is an error in this word";
let text = "text";
let range = 0..usize::MAX;
let commented_string = format_like!(
parse_str,
[('{', parse_interpolation, false), ('<', parse_comment, true)],
CommentedString(String::new(), Vec::new()),
"This is <comment>worng {}, and this is the end of the range {range.end}",
text
);
In this example, the {}
should work as intended, but you also
have access to <>
interpolation. Inside <>
, a comment will be
added, with the associated usize
being its position in the
String
.
This will all be done through the parse_str
,
parse_interpolation
and parse_comment
macros:
#![feature(decl_macro)]
macro parse_str($value:expr, $str:literal) {{
let mut commented_string = $value;
commented_string.0.push_str($str);
commented_string
}}
macro parse_interpolation($value:expr, $added:expr, $modif:literal) {{
let CommentedString(string, comments) = $value;
let string = format!(concat!("{}{", $modif, "}"), string, $added);
CommentedString(string, comments)
}}
macro parse_comment($value:expr, $added:expr, $_modif:literal) {{
let mut commented_string = $value;
commented_string
.1
.push((commented_string.0.len(), $added.to_string()));
commented_string
}}
The parse_str
macro will be responsible for handling the non
{}
or <>
parts of the literal &str
. The parse_comment
and
parse_interpolation
methods will handle what’s inside the <>
and {}
pairs, respectively.
parse_comment
and parse_interpolation
must have three
parameters, one for the value
being modified (in this case, a
CommentedString
), one for the object being added (it’s
Display
objects in this case, but it could be anything else),
and a modifier ("?", "#?", ".3", etc), which might come after a
“:”` is found in the pair.
Now, as I mentioned earlier, this crate is meant for you to create your own format like macros, so you should package all of this up into a single macro, like this:
#![feature(decl_macro)]
use format_like::format_like;
#[derive(Debug, PartialEq)]
struct CommentedString(String, Vec<(usize, String)>);
let comment = "there is an error in this word";
let text = "text";
let range = 0..usize::MAX;
let commented_string = commented_string!(
"This is <comment>worng {}, and this is the end of the range {range.end}",
text
);
assert_eq!(
commented_string,
CommentedString(
"This is worng text, and this is the end of the range 18446744073709551615".to_string(),
vec![(8, "there is an error in this word".to_string())]
)
);
macro commented_string($($parts:tt)*) {
format_like!(
parse_str,
[('{', parse_interpolation, false), ('<', parse_comment, true)],
CommentedString(String::new(), Vec::new()),
$($parts)*
)
}
macro parse_str($value:expr, $str:literal) {{
let mut commented_string = $value;
commented_string.0.push_str($str);
commented_string
}}
macro parse_interpolation($value:expr, $added:expr, $modif:literal) {{
let CommentedString(string, comments) = $value;
let string = format!(concat!("{}{", $modif, "}"), string, $added);
CommentedString(string, comments)
}}
macro parse_comment($value:expr, $added:expr, $_modif:literal) {{
let mut commented_string = $value;
commented_string.1.push((commented_string.0.len(), $added.to_string()));
commented_string
}}
Forced inlining
You might be wondering: What are the false
and true
in the
second argument of format_like!
used for?
Well, they determine wether an argument must be inlined (i.e. be
placed within the string like {arg}
). This is useful when you
want to limit the types of arguments that a macro should handle.
As you might have seen earlier, format_like!
accepts member
access, like {range.end}
. If you force a parameter to always be
placed inline, that limits the types of tokens your macro must be
able to handle, so you could rewrite the parse_comment
macro to
be:
#![feature(decl_macro)]
macro parse_comment($value:expr, $($identifier:ident).*, $modif:literal) {{
// innards
}}
While this may not seem useful, it comes with two interesting abilities:
1 - If arguments must be inlined, you are allowed to leave the
pair empty, like <>
, and you can handle this situation
differently if you want.
2 - By accessing the $identifiers
directly, you can manipulate
them in whichever way you want, heck, they may not even point to
any actual variable in the code, and could be some sort of
differently handled string literal.
Motivation
Even after reading all that, I wouldn’t be surprised if you haven’t found any particular use for this crate, and that’s fine.
But here is what was my motivation for creating it:
In my in development text editor Duat, there used to be a
text
macro, which created a Text
struct, which was essentially
a String
with formatting Tag
s added on to it.
It used to work like this:
let text = text!("start" [RedColor] variable " " other_variable " ");
This macro was a simple declarative macro, so while it was easy to implement, there were several drawbacks to its design:
- It was ignored by rustfmt;
- It didn’t look like Rust;
- tree-sitter failed at syntax highlighting it;
- It didn’t look like Rust;
- Way too much space was occupied by simple things like
" "
; - It didn’t look like Rust;
And now I have replaced the old text
macro with a new version,
based on format_like!
, which makes for a much cleaner design:
let text = text!("start [RedColor]{variable} {other_variable} ");
Dependencies
~200–630KB
~15K SLoC