Skip to main content

sup_xml_core/xsd/
whitespace.rs

1//! XSD whiteSpace facet — `preserve` / `replace` / `collapse`.
2//!
3//! Whitespace handling runs **before** any other facet check.  `xs:string`
4//! defaults to `preserve`; `xs:normalizedString` to `replace`;
5//! `xs:token` and most other types to `collapse`.
6
7/// The three whitespace-handling modes defined by XSD §4.3.6.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum WhitespaceMode {
10    /// Leave the lexical value as-is.
11    Preserve,
12    /// Replace each tab/CR/LF with a single space.  No collapsing.
13    Replace,
14    /// `Replace`, then collapse runs of spaces to one and trim leading and
15    /// trailing spaces.
16    Collapse,
17}
18
19impl WhitespaceMode {
20    /// Apply this mode to a raw lexical value.
21    pub fn apply<'a>(self, s: &'a str) -> std::borrow::Cow<'a, str> {
22        use std::borrow::Cow;
23        match self {
24            WhitespaceMode::Preserve => Cow::Borrowed(s),
25            WhitespaceMode::Replace => {
26                if s.bytes().any(|b| matches!(b, b'\t' | b'\n' | b'\r')) {
27                    let out: String = s.chars()
28                        .map(|c| if matches!(c, '\t' | '\n' | '\r') { ' ' } else { c })
29                        .collect();
30                    Cow::Owned(out)
31                } else {
32                    Cow::Borrowed(s)
33                }
34            }
35            WhitespaceMode::Collapse => {
36                // Replace + collapse + trim.  Done in one pass.
37                let needs_work = s.bytes().any(|b| matches!(b, b'\t' | b'\n' | b'\r'))
38                    || s.contains("  ")
39                    || s.starts_with(' ')
40                    || s.ends_with(' ');
41                if !needs_work {
42                    return Cow::Borrowed(s);
43                }
44                let mut out = String::with_capacity(s.len());
45                let mut prev_space = true; // pretend we started after a space → trim leading
46                for c in s.chars() {
47                    let is_ws = matches!(c, ' ' | '\t' | '\n' | '\r');
48                    if is_ws {
49                        if !prev_space {
50                            out.push(' ');
51                            prev_space = true;
52                        }
53                    } else {
54                        out.push(c);
55                        prev_space = false;
56                    }
57                }
58                if out.ends_with(' ') {
59                    out.pop();
60                }
61                Cow::Owned(out)
62            }
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn preserve_passes_through() {
73        let s = "  a\tb\n  c  ";
74        assert_eq!(WhitespaceMode::Preserve.apply(s), s);
75    }
76
77    #[test]
78    fn replace_only_swaps_ws_chars() {
79        assert_eq!(WhitespaceMode::Replace.apply("a\tb\nc"), "a b c");
80        assert_eq!(WhitespaceMode::Replace.apply("  hello  "), "  hello  ");
81    }
82
83    #[test]
84    fn collapse_normalizes() {
85        assert_eq!(WhitespaceMode::Collapse.apply("  a   b\t\n c  "), "a b c");
86        assert_eq!(WhitespaceMode::Collapse.apply("plain"), "plain");
87        assert_eq!(WhitespaceMode::Collapse.apply(""), "");
88        assert_eq!(WhitespaceMode::Collapse.apply("   "), "");
89    }
90
91    #[test]
92    fn collapse_borrows_when_clean() {
93        use std::borrow::Cow;
94        let result = WhitespaceMode::Collapse.apply("clean");
95        assert!(matches!(result, Cow::Borrowed("clean")));
96    }
97}