Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ fn main() {
// Debug formatting
let result = formatx!("{:?}", "a string").unwrap();
println!("{result}");
}
}
6 changes: 4 additions & 2 deletions examples/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ fn main() {

// Template reuse
let template = formatx::Template::new("Point {name}: {point}").unwrap();
let r1 = template.render()
let r1 = template
.render()
.named("name", &"Origin")
.named("point", &origin)
.finish()
.unwrap();
let r2 = template.render()
let r2 = template
.render()
.named("name", &"Target")
.named("point", &target)
.finish()
Expand Down
40 changes: 20 additions & 20 deletions images/benchmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 7 additions & 11 deletions src/ast.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Typed AST for parsed format strings.

use alloc::vec::Vec;

/// Byte range in the source format string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
Expand Down Expand Up @@ -51,7 +53,7 @@ pub enum Argument {
}

/// The full format specification after `:` inside a placeholder.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct FormatSpec {
pub fill: Option<char>,
pub align: Option<Align>,
Expand Down Expand Up @@ -82,13 +84,7 @@ impl FormatSpec {
/// meaning we can take the fast path and `write!` directly.
#[inline]
pub fn is_default(&self) -> bool {
self.fill.is_none()
&& self.align.is_none()
&& self.sign.is_none()
&& !self.alternate
&& !self.zero_pad
&& self.width.is_none()
&& self.precision.is_none()
self == &Self::default()
}
}

Expand All @@ -108,7 +104,7 @@ pub enum Sign {
}

/// A width or precision value - either a literal number or a parameter reference.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum Count {
/// A literal integer, e.g. `10` in `{:10}`.
Literal(usize),
Expand All @@ -117,7 +113,7 @@ pub enum Count {
}

/// A parameter reference for width/precision.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum CountParam {
/// `{:0$}` - positional argument index.
Positional(usize),
Expand All @@ -126,7 +122,7 @@ pub enum CountParam {
}

/// Precision specification.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum Precision {
/// `.5` or `.prec$` - a count value.
Count(Count),
Expand Down
22 changes: 7 additions & 15 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
//! Error types for formatx.

use crate::ast::{FormatType, Span};
use std::fmt;
use alloc::string::String;
use core::fmt;

/// Errors that can occur during parsing or formatting.
#[derive(Debug)]
pub enum Error {
/// The format string could not be parsed.
Parse {
span: Span,
message: String,
},
Parse { span: Span, message: String },
/// A placeholder references an argument that was not provided.
MissingArgument {
name: String,
span: Span,
},
MissingArgument { name: String, span: Span },
/// A format type (e.g. `{:x}`) requires a trait we don't support.
UnsupportedTrait {
format_type: FormatType,
span: Span,
},
UnsupportedTrait { format_type: FormatType, span: Span },
/// An underlying `std::fmt::Error` occurred during formatting.
Format(fmt::Error),
}
Expand Down Expand Up @@ -52,8 +44,8 @@ impl fmt::Display for Error {
}
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
impl core::error::Error for Error {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
if let Self::Format(e) = self {
Some(e)
} else {
Expand Down
66 changes: 46 additions & 20 deletions src/format.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
//! Format engine - applies [`FormatSpec`] to produce formatted output.

use crate::{ast::*, error::Error, value::FormatValue};
use std::fmt::{Debug, Write};
use crate::{
ast::*,
error::Error,
value::{FormatArg, FormatValue},
};
use alloc::{
format,
string::{String, ToString},
};
use core::fmt::{Debug, Write};

type NamedArgs<'a> = [(&'a str, usize)];

/// Render a parsed [`FormatString`] into `output` using the provided arguments.
pub fn render(
output: &mut String,
source: &str,
parsed: &FormatString,
args: &[&dyn FormatValue],
named: &[(&str, usize)],
args: &[FormatArg],
named: &NamedArgs,
strict: bool,
) -> Result<(), Error> {
let mut implicit_pos: usize = 0;
Expand All @@ -36,7 +46,7 @@ pub fn render(
resolve_argument(&placeholder.argument, source, &mut implicit_pos, named);

let arg = match arg_index {
Some(idx) if idx < args.len() => Some(args[idx]),
Some(idx) if idx < args.len() => Some(args[idx].as_value()),
Some(_) | None => None,
};

Expand Down Expand Up @@ -102,7 +112,7 @@ fn resolve_argument(
argument: &Argument,
source: &str,
implicit_pos: &mut usize,
named: &[(&str, usize)],
named: &NamedArgs,
) -> Option<usize> {
match argument {
Argument::Implicit => {
Expand All @@ -121,8 +131,8 @@ fn resolve_argument(
fn resolve_count_value(
count: &Option<Count>,
source: &str,
args: &[&dyn FormatValue],
named: &[(&str, usize)],
args: &[FormatArg],
named: &NamedArgs,
) -> Result<Option<usize>, Error> {
let Some(count) = count else { return Ok(None) };
match count {
Expand All @@ -148,7 +158,7 @@ fn resolve_count_value(
message: format!("count argument index {idx} out of range"),
});
}
let formatted = format!("{}", args[idx]);
let formatted = format!("{}", args[idx].as_value());
formatted
.parse::<usize>()
.map(Some)
Expand All @@ -163,8 +173,8 @@ fn resolve_count_value(
fn resolve_precision(
precision: &Option<Precision>,
source: &str,
args: &[&dyn FormatValue],
named: &[(&str, usize)],
args: &[FormatArg],
named: &NamedArgs,
implicit_pos: &mut usize,
) -> Result<Option<usize>, Error> {
let Some(prec) = precision else {
Expand All @@ -181,7 +191,7 @@ fn resolve_precision(
message: "not enough arguments for `.*` precision".to_string(),
});
}
let formatted = format!("{}", args[idx]);
let formatted = format!("{}", args[idx].as_value());
formatted
.parse::<usize>()
.map(Some)
Expand Down Expand Up @@ -292,10 +302,18 @@ fn format_full(
(true, false, _, None, Some(p)) => write!(output, "{:+.prec$?}", dbg, prec = p),
(false, true, _, None, Some(p)) => write!(output, "{:#.prec$?}", dbg, prec = p),
(true, true, _, None, Some(p)) => write!(output, "{:+#.prec$?}", dbg, prec = p),
(false, false, false, Some(w), None) => write!(output, "{:width$?}", dbg, width = w),
(true, false, false, Some(w), None) => write!(output, "{:+width$?}", dbg, width = w),
(false, true, false, Some(w), None) => write!(output, "{:#width$?}", dbg, width = w),
(true, true, false, Some(w), None) => write!(output, "{:+#width$?}", dbg, width = w),
(false, false, false, Some(w), None) => {
write!(output, "{:width$?}", dbg, width = w)
}
(true, false, false, Some(w), None) => {
write!(output, "{:+width$?}", dbg, width = w)
}
(false, true, false, Some(w), None) => {
write!(output, "{:#width$?}", dbg, width = w)
}
(true, true, false, Some(w), None) => {
write!(output, "{:+#width$?}", dbg, width = w)
}
(false, false, false, Some(w), Some(p)) => {
write!(output, "{:width$.prec$?}", dbg, width = w, prec = p)
}
Expand All @@ -308,10 +326,18 @@ fn format_full(
(true, true, false, Some(w), Some(p)) => {
write!(output, "{:+#width$.prec$?}", dbg, width = w, prec = p)
}
(false, false, true, Some(w), None) => write!(output, "{:0width$?}", dbg, width = w),
(true, false, true, Some(w), None) => write!(output, "{:+0width$?}", dbg, width = w),
(false, true, true, Some(w), None) => write!(output, "{:#0width$?}", dbg, width = w),
(true, true, true, Some(w), None) => write!(output, "{:+#0width$?}", dbg, width = w),
(false, false, true, Some(w), None) => {
write!(output, "{:0width$?}", dbg, width = w)
}
(true, false, true, Some(w), None) => {
write!(output, "{:+0width$?}", dbg, width = w)
}
(false, true, true, Some(w), None) => {
write!(output, "{:#0width$?}", dbg, width = w)
}
(true, true, true, Some(w), None) => {
write!(output, "{:+#0width$?}", dbg, width = w)
}
(false, false, true, Some(w), Some(p)) => {
write!(output, "{:0width$.prec$?}", dbg, width = w, prec = p)
}
Expand Down
12 changes: 11 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg_attr(not(test), no_std)]

//! `formatx` lets you format strings at runtime using the same syntax as
//! [`std::fmt`] (`{}`, `{:?}`, `{name}`, etc.), but with runtime template
//! strings instead of compile-time literals with **zero** dependencies.
Expand Down Expand Up @@ -42,6 +44,8 @@
//! assert_eq!(r2, "Bob has 7 items");
//! ```

extern crate alloc;

mod ast;
mod error;
mod format;
Expand All @@ -51,8 +55,14 @@ mod renderer;
mod template;
mod value;

#[doc(hidden)]
pub mod __private {
pub use alloc::string::String;
pub use core::result::Result;
}

pub use ast::FormatType;
pub use error::Error;
pub use renderer::Renderer;
pub use template::Template;
pub use value::FormatValue;
pub use value::{FormatArg, FormatValue, IntoFormatArg};
8 changes: 4 additions & 4 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
#[macro_export]
macro_rules! formatx {
($template:expr $(,)?) => {
(|| -> ::std::result::Result<::std::string::String, $crate::Error> {
(|| -> $crate::__private::Result<$crate::__private::String, $crate::Error> {
$crate::Template::new($template)?.render().finish()
})()
};
($template:expr, $($args:tt)*) => {
(|| -> ::std::result::Result<::std::string::String, $crate::Error> {
(|| -> $crate::__private::Result<$crate::__private::String, $crate::Error> {
let t = $crate::Template::new($template)?;
let mut r = t.render();
$crate::_formatx_internal!(r, $($args)*);
Expand All @@ -52,12 +52,12 @@ macro_rules! formatx {
#[macro_export]
macro_rules! formatxl {
($template:expr $(,)?) => {
(|| -> ::std::result::Result<::std::string::String, $crate::Error> {
(|| -> $crate::__private::Result<$crate::__private::String, $crate::Error> {
$crate::Template::new($template)?.render().finish_lenient()
})()
};
($template:expr, $($args:tt)*) => {
(|| -> ::std::result::Result<::std::string::String, $crate::Error> {
(|| -> $crate::__private::Result<$crate::__private::String, $crate::Error> {
let t = $crate::Template::new($template)?;
let mut r = t.render();
$crate::_formatx_internal!(r, $($args)*);
Expand Down
1 change: 1 addition & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Single-pass parser for `std::fmt` - style format strings.

use crate::{ast::*, error::Error};
use alloc::{format, string::ToString, vec::Vec};

/// Parse a format string into a [`FormatString`] AST.
pub fn parse(source: &str) -> Result<FormatString, Error> {
Expand Down
Loading