#text #macro #format #decl-macro #string #output #partial-eq #push-str #part #commented-string

macro format-like

Macro for creating format-like macros with any kind of output

2 unstable releases

Uses new Rust 2024

new 0.2.0 May 5, 2025
0.1.0 May 5, 2025

#459 in Value formatting

AGPL-3.0-or-later

31KB
299 lines

format-like License: AGPL-3.0-or-later format-like on crates.io format-like on docs.rs

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 Tags 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