procss/ast/token/
space.rs

1// ┌───────────────────────────────────────────────────────────────────────────┐
2// │                                                                           │
3// │  ██████╗ ██████╗  ██████╗   Copyright (C) 2022, The Prospective Company   │
4// │  ██╔══██╗██╔══██╗██╔═══██╗                                                │
5// │  ██████╔╝██████╔╝██║   ██║  This file is part of the Procss library,      │
6// │  ██╔═══╝ ██╔══██╗██║   ██║  distributed under the terms of the            │
7// │  ██║     ██║  ██║╚██████╔╝  Apache License 2.0.  The full license can     │
8// │  ╚═╝     ╚═╝  ╚═╝ ╚═════╝   be found in the LICENSE file.                 │
9// │                                                                           │
10// └───────────────────────────────────────────────────────────────────────────┘
11
12use nom::branch::alt;
13use nom::bytes::complete::{is_not, tag};
14use nom::character::complete::{anychar, multispace1};
15use nom::error::ParseError;
16use nom::multi::{many0, many1, many_till};
17use nom::sequence::preceded;
18use nom::IResult;
19
20/// An "extension" trait for [`str`] which is used frequently to determine
21/// whether whitepsace can be removed during [`crate::render::RenderCss`]
22pub trait NeedsWhitespaceStringExt {
23    /// Does this string needs a leading whitespace character?
24    fn needs_pre_ws(&self) -> bool;
25
26    /// Does this string needs a trailing whitespace character?
27    fn needs_post_ws(&self) -> bool;
28}
29
30impl NeedsWhitespaceStringExt for str {
31    fn needs_pre_ws(&self) -> bool {
32        self.chars()
33            .next()
34            .map(|x| {
35                x.is_ascii_alphanumeric()
36                    || x == '-'
37                    || x == '_'
38                    || x == '%'
39                    || x == '+'
40                    || x == '"'
41                    || x == '\''
42                    || x == '('
43            })
44            .unwrap_or_default()
45    }
46
47    fn needs_post_ws(&self) -> bool {
48        self.chars()
49            .last()
50            .map(|x| {
51                x.is_ascii_alphanumeric()
52                    || x == '"'
53                    || x == '\''
54                    || x == '-'
55                    || x == '_'
56                    || x == '%'
57                    || x == '+'
58                    || x == ')'
59            })
60            .unwrap_or_default()
61    }
62}
63
64/// Render `s` trimming all intermediate whitespace to a single character along
65/// the way.
66pub fn trim_whitespace(s: &str, f: &mut std::fmt::Formatter<'_>) {
67    let mut last_alpha = false;
68    s.split_whitespace().for_each(|w| {
69        if last_alpha && w.needs_pre_ws() {
70            write!(f, " ").unwrap();
71        }
72
73        last_alpha = w.needs_post_ws();
74        write!(f, "{}", w).unwrap();
75    });
76}
77
78fn parse_comment<'a, E>(input: &'a str) -> IResult<&'a str, (), E>
79where
80    E: ParseError<&'a str>,
81{
82    ignore(preceded(tag("//"), many0(is_not("\r\n"))))(input)
83}
84
85fn parse_multi_comment<'a, E>(input: &'a str) -> IResult<&'a str, (), E>
86where
87    E: ParseError<&'a str>,
88{
89    ignore(preceded(tag("/*"), many_till(anychar, tag("*/"))))(input)
90}
91
92fn ignore<'a, T, E, F>(mut f: F) -> impl FnMut(&'a str) -> IResult<&'a str, (), E>
93where
94    F: FnMut(&'a str) -> IResult<&'a str, T, E>,
95{
96    move |input| {
97        let (input, _) = f(input)?;
98        Ok((input, ()))
99    }
100}
101
102/// Parses 0 or more whitespace characters, including comments.
103pub fn comment0<'a, E>(input: &'a str) -> IResult<&'a str, (), E>
104where
105    E: ParseError<&'a str>,
106{
107    let (input, _) = many0(alt((
108        ignore(multispace1),
109        parse_comment,
110        parse_multi_comment,
111    )))(input)?;
112    Ok((input, ()))
113}
114
115/// Parses 1 or more whitespace characters, including comments.
116pub fn comment1<'a, E>(input: &'a str) -> IResult<&'_ str, (), E>
117where
118    E: ParseError<&'a str>,
119{
120    ignore(many1(alt((
121        ignore(multispace1),
122        parse_comment,
123        parse_multi_comment,
124    ))))(input)
125}
126
127/// Parses 0 or more whitespace characters, including comments and semicolons.
128pub fn sep0<'a, E>(input: &'a str) -> IResult<&'_ str, (), E>
129where
130    E: ParseError<&'a str>,
131{
132    ignore(many0(alt((comment1, ignore(tag(";"))))))(input)
133}
134
135#[cfg(test)]
136mod tests {
137    use std::assert_matches::assert_matches;
138
139    use super::*;
140
141    #[test]
142    fn test_multiline_comment() {
143        assert_matches!(
144            comment0::<()>(
145                "
146    /* 
147     * test
148     */"
149            ),
150            Ok(("", ()))
151        )
152    }
153
154    #[test]
155    fn test_forward_slash() {
156        assert_matches!(comment0::<()>("// test"), Ok(("", ())))
157    }
158
159    #[test]
160    fn test_semicolons() {
161        assert_matches!(comment0::<()>("/* test; test */"), Ok(("", ())))
162    }
163}