Skip to main content

ical/tree/param/
node.rs

1//! # Parameter node
2//!
3//! The raw, byte-faithful parameter on the syntax side.
4//!
5//! [`IcalParamNode`] is the syntactic peer of the decoded
6//! [`IcalParam`](crate::param::IcalParam): a name leaf and its raw value
7//! leaves, parsed from and serialized back to the wire verbatim. The per-name
8//! parameter lenses that give it meaning live alongside in
9//! [`crate::tree::param`].
10
11use core::fmt;
12
13use alloc::vec::Vec;
14
15use crate::tree::{codec::mode::Escaper, leaf::IcalLeaf};
16
17/// One raw parameter: a name and its `,`-separated raw values (empty when the
18/// parameter has no `=` list). The syntactic peer of the decoded
19/// [`IcalParam`](crate::param::IcalParam).
20#[derive(Clone, Debug)]
21pub struct IcalParamNode<'a> {
22    /// The parameter name leaf.
23    pub name: IcalLeaf<'a>,
24    /// The raw value leaves.
25    pub values: Vec<IcalLeaf<'a>>,
26    /// Which version's parameter encoding rules the values are written in.
27    /// Stamped by the parser once `VERSION` is known, as on a value node.
28    pub escaper: Escaper,
29}
30
31impl<'a> IcalParamNode<'a> {
32    /// Parse one `name=value,value` parameter (commas outside quotes split).
33    pub fn parse(param: &'a str) -> Self {
34        match param.split_once('=') {
35            Some((name, values)) => Self {
36                name: IcalLeaf::from(name),
37                values: split_param_values(values)
38                    .into_iter()
39                    .map(IcalLeaf::from)
40                    .collect(),
41                escaper: Escaper::default(),
42            },
43            None => Self {
44                name: IcalLeaf::from(param),
45                values: Vec::new(),
46                escaper: Escaper::default(),
47            },
48        }
49    }
50
51    /// Convert into an owned parameter node (`'static`).
52    pub(crate) fn into_static(self) -> IcalParamNode<'static> {
53        IcalParamNode {
54            name: self.name.into_static(),
55            values: self.values.into_iter().map(IcalLeaf::into_static).collect(),
56            escaper: self.escaper,
57        }
58    }
59
60    /// Serialize the parameter (`name` or `name=value,value`) into `out`,
61    /// without the intermediate `String` a `Display`-based path would allocate.
62    pub(crate) fn write_bytes(&self, out: &mut Vec<u8>) {
63        out.extend_from_slice(self.name.get().as_bytes());
64
65        if let Some((first, rest)) = self.values.split_first() {
66            out.push(b'=');
67            out.extend_from_slice(first.get().as_bytes());
68            for value in rest {
69                out.push(b',');
70                out.extend_from_slice(value.get().as_bytes());
71            }
72        }
73    }
74}
75
76impl fmt::Display for IcalParamNode<'_> {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(self.name.get())?;
79
80        if let Some((first, rest)) = self.values.split_first() {
81            write!(f, "={}", first.get())?;
82
83            for value in rest {
84                write!(f, ",{}", value.get())?;
85            }
86        }
87
88        Ok(())
89    }
90}
91
92/// Split a parameter value list on commas outside double quotes.
93fn split_param_values(values: &str) -> Vec<&str> {
94    let bytes = values.as_bytes();
95    let mut pieces = Vec::new();
96    let mut start = 0;
97    let mut quoted = false;
98
99    for (i, &byte) in bytes.iter().enumerate() {
100        match byte {
101            b'"' => quoted = !quoted,
102            b',' if !quoted => {
103                pieces.push(&values[start..i]);
104                start = i + 1;
105            }
106            _ => {}
107        }
108    }
109
110    pieces.push(&values[start..]);
111    pieces
112}
113
114#[cfg(test)]
115mod tests {
116    use alloc::string::ToString;
117
118    use crate::tree::param::node::IcalParamNode;
119
120    #[test]
121    fn parses_quoted_values_then_round_trips() {
122        let node = IcalParamNode::parse(r#"TYPE=work,"a,b""#);
123        assert_eq!(node.values.len(), 2);
124        assert_eq!(node.to_string(), r#"TYPE=work,"a,b""#);
125    }
126}