streplace

Crates.iostreplace
lib.rsstreplace
version3.0.0
created_at2025-05-21 00:21:50.534232+00
updated_at2025-07-10 21:26:37.468488+00
descriptionA tiny library for matching and replacing in strings and slices with user-defined functions.
homepage
repositoryhttps://gitlab.com/MayESchaefer/streplace
max_upload_size
id1682711
size34,421
Mabel Schaefer (Hate9)

documentation

README

STReplace

crates.io docs

STReplace is a small library for matching and replacing in strings and slices with user-defined functions. It does not currently support overlapping matches.

It provides several extension methods to the String and &str types, for this purpose.

This crate also includes several feature flags:

  • result: Provides variants of get_matches, replace_matches, and match_and_replace which accept functions that return Result values.
  • convenience: Provides additional convenience Matcher and Replacer implementations, which can be used in place of bare functions.

Benchmarks

On a very basic benchmark, based on the first example in the section below:

This benchmark is now several versions out of date, but should remain approximately accurate.

Examples

use streplace::{InProgressMatch, Match, MatchResult, Matchable};

fn main() {
    let test = "hello test, this is a test string";
    let new = test.match_and_replace(
        |progress, index, character| match progress {
            Some(InProgressMatch { start, value }) => {
                if index - start >= 4 {
                    println!("end {start} {value} {character}");
                    MatchResult::End
                } else if "test"
                    .chars()
                    .nth(index - start)
                    .is_some_and(|c| character == c)
                {
                    println!("cont {start} {value}{character}");
                    MatchResult::Continue
                } else {
                    println!("fail {start} {value} {character}");
                    MatchResult::Fail
                }
            }
            None => {
                if character == 't' {
                    println!("start {index} {character}");
                    MatchResult::Start
                } else {
                    println!("none {index} {character}");
                    MatchResult::None
                }
            }
        },
        |Match { start, end, value }| format!("start:{start},end:{end},val:{value}"),
        false,
    );
    println!("{new}");
}
use streplace::{
    AccumulatorMatcher, ChainMatchable, InProgressMatch, Match, MatchResult, Matchable,
};

fn main() {
    let text = "@first_of(a b c)";
    let text = text.match_and_replace(
        "@first_of(".chain(AccumulatorMatcher::new(
            0,
            |progress, index, character, parens| {
                let value = progress.map(|p| p.value);
                let mut last_chars = value.unwrap_or_default().chars().rev();
                let last_char = last_chars.next();
                let second_last_char = last_chars.next();
                if character == '(' && last_char != Some('\\') {
                    *parens += 1;
                }
                if last_char.is_some_and(|c| c == ')') && second_last_char != Some('\\') {
                    println!("last char is paren");
                    if *parens == 0 {
                        *parens = 0;
                        MatchResult::End
                    } else {
                        *parens -= 1;
                        MatchResult::Continue
                    }
                } else if index == text.len() - 1 && !(character == ')' && last_char != Some('\\'))
                {
                    *parens = 0;
                    MatchResult::Fail
                } else {
                    MatchResult::Continue
                }
            },
        )),
        |Match {
             start: _,
             end: _,
             value,
         }| {
            let substring = &value["@first_of(".len()..value.len() - 1];

            let submatches: Vec<&str> = substring
                .get_matches(
                    AccumulatorMatcher::new(0, |progress, _, character, parens| {
                        match (progress, character) {
                            (Some(_), ' ') => {
                                if *parens == 0 {
                                    MatchResult::End
                                } else {
                                    MatchResult::Continue
                                }
                            }
                            (
                                Some(InProgressMatch {
                                    start: _,
                                    value: match_value,
                                }),
                                '(',
                            ) => {
                                if match_value.ends_with('\\') {
                                    MatchResult::Continue
                                } else {
                                    *parens += 1;
                                    MatchResult::Continue
                                }
                            }
                            (
                                Some(InProgressMatch {
                                    start: _,
                                    value: match_value,
                                }),
                                ')',
                            ) => {
                                if match_value.ends_with('\\') {
                                    MatchResult::Continue
                                } else {
                                    *parens = (*parens - 1).max(0);
                                    MatchResult::Continue
                                }
                            }
                            (Some(_), _) => MatchResult::Continue,
                            (None, ' ') => {
                                *parens = 0;
                                MatchResult::None
                            }
                            (None, _) => {
                                *parens = 0;
                                MatchResult::Start
                            }
                        }
                    }),
                    true,
                )
                .into_iter()
                .map(|m| m.value)
                .collect();

            let chosen = submatches
                .first()
                .map(|v| v.to_string())
                .unwrap_or_default();
            chosen
        },
        true,
    );

    println!("{text}");
}

Changelog

  • 3.0.0
    • Replaced the match and replace functions with generic Matcher, Replacer, TryMatcher, and TryReplacer functions
    • Removed get_matches_acc, match_and_replace_acc, and try_match_and_replace_acc functions, and replaced them with Matcher implementations
    • Added convenience implementations of Matcher and Replacer for Strings
    • Added AccumulatorMatcher and ChainMatcher convenience Matchers, TryMatcher variants of those, and the TryMatcherWrapper TryMatcher
    • Modularized most of the code and moved some modules to feature flags (enabled by default)
  • 2.2.1
    • Fixed an error which would occur when traversing strings containing characters spanning more than one byte
  • 2.2.0
    • Added try_replace_matches, try_match_and_replace, and try_match_and_replace_acc functions
  • 2.1.1
    • Fixed a major bug in the replacer functions
  • 2.1.0
    • Added get_matches_acc and match_and_replace_acc functions
  • 2.0.0
    • Added match_end parameter to matching functions
  • 1.0.1
    • Implemented Matchable for String
  • 1.0.0
    • Initial release
Commit count: 23

cargo fmt