pochoir-lang 0.12.2

Custom parser and interpreter for the pochoir template engine
Documentation
//! Remove trailing characters in a string.
//!
//! The default removed trailing characters are whitespace (including newlines) but it is possible
//! to configure them using the argument.
//!
//! Structure of the object taken as argument:
//!
//! | Field   | Type            | Default value                              |  Description                                       |
//! | ------- | --------------- | ------------------------------------------ | -------------------------------------------------- |
//! | start   | Bool            | true                                       | whether starting trailing characters are removed   |
//! | end     | Bool            | true                                       | whether ending trailing characters are removed     |
//! | matches | Array or String | Unicode `White_Space` (including newlines) | list of characters (or single character) to remove |
//!
//! ### Example
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="border-left: 2px solid red;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">input</span><code><span class="fn">trim</span>(<span class="string">"  hello  "</span>)</code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="border-left: 2px solid red;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">output</span><code><span class="string">"hello"</span></code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="position: relative; margin-top: 2rem; border-left: 2px solid blue;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">input</span><code><span class="fn">trim</span>(<span class="string">"  hello  "</span>, { start: <span class="bool-val">false</span> })</code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="border-left: 2px solid blue;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">output</span><code><span class="string">"  hello"</span></code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="position: relative; margin-top: 2rem; border-left: 2px solid green;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">input</span><code><span class="fn">trim</span>(<span class="string">"11hello22"</span>, { matches: [<span class="string">"1"</span>, <span class="string">"2"</span>] })</code></pre></div>
//!
//! <div class="example-wrap"><pre class="rust rust-example-rendered" style="border-left: 2px solid green;"><span style="position: absolute; right: 0; top: 0; padding: 0.1rem 0.4rem 0 0; font-size: 0.75em; font-weight: bold; color: #333;">output</span><code><span class="string">"hello"</span></code></pre></div>
use crate::{FromValue, FunctionResult, Value};

pub(crate) struct Opts {
    start: Option<bool>,
    end: Option<bool>,
    matches: Option<Value>,
}

impl FromValue for Opts {
    fn from_value(val: Value) -> Result<Self, Box<dyn std::error::Error>> {
        if val == Value::Null {
            Ok(Self {
                start: None,
                end: None,
                matches: None,
            })
        } else if let Value::Object(mut val) = val {
            Ok(Self {
                start: if let Some(val) = val.remove("start") {
                    Some(bool::from_value(val)?)
                } else {
                    None
                },
                end: if let Some(val) = val.remove("end") {
                    Some(bool::from_value(val)?)
                } else {
                    None
                },
                matches: val.remove("matches"),
            })
        } else {
            Err(Box::new(crate::Error::MismatchedTypes {
                expected: "Object".to_string(),
                found: val.type_name().to_string(),
            }))
        }
    }
}

pub(crate) fn trim(val: String, opts: Option<Opts>) -> FunctionResult<String> {
    let opts = opts.unwrap_or(Opts {
        start: None,
        end: None,
        matches: None,
    });
    let start = opts.start.unwrap_or(true);
    let end = opts.end.unwrap_or(true);

    if let Some(matches) = opts.matches {
        if let Value::String(matches) = matches {
            let mut val = if start {
                val.trim_start_matches(&matches)
            } else {
                &val
            };

            if end {
                val = val.trim_end_matches(&matches);
            }

            Ok(val.to_string())
        } else if let Value::Array(matches) = matches {
            let matches = matches
                .iter()
                .filter_map(|m| {
                    if let Value::String(m) = m {
                        m.chars().next()
                    } else {
                        None
                    }
                })
                .collect::<Vec<char>>();
            let mut val = if start {
                val.trim_start_matches(|ch| matches.contains(&ch))
            } else {
                &val
            };

            if end {
                val = val.trim_end_matches(|ch| matches.contains(&ch));
            }

            Ok(val.to_string())
        } else {
            Err(format!(
                "mismatched types for the object field `matches` passed as argument of the `trim` function: expected String or Array, found {}",
                matches.type_name()
            ).into())
        }
    } else {
        let mut val = if start { val.trim_start() } else { &val };

        if end {
            val = val.trim_end();
        }

        Ok(val.to_string())
    }
}

#[cfg(test)]
mod tests {
    use crate::IntoValue;

    use super::*;

    #[test]
    fn trim_test() {
        assert_eq!(
            trim("  hello  ".to_string(), None).unwrap(),
            "hello".to_string()
        );
        assert_eq!(
            trim(
                "  hello  ".to_string(),
                Some(Opts {
                    start: Some(false),
                    end: None,
                    matches: None,
                }),
            )
            .unwrap(),
            "  hello".to_string()
        );
        assert_eq!(
            trim(
                "11hello22".to_string(),
                Some(Opts {
                    start: None,
                    end: None,
                    matches: Some(
                        vec![Value::String("1".to_string()), "2".into_value()].into_value()
                    ),
                }),
            )
            .unwrap(),
            "hello".to_string()
        );
    }
}