Skip to main content

ical/tree/codec/
encode.rs

1//! # Encode (model to syntax)
2//!
3//! The write side of the structural bridge: project the decoded model onto a
4//! raw syntax tree. A value's [`Codec`] impl encodes it into a
5//! [`IcalValueNode`], an [`IcalParam`] encodes into an [`IcalParamNode`], a
6//! [`IcalProp`] encodes into an [`IcalLine`], an [`IcalComponent`] encodes into a
7//! nested [`IcalCst`], and an [`Ical`] encodes into the whole `VCALENDAR`
8//! [`IcalCst`] (recursively). The whole calendar is encoded for its version's
9//! [`Escaper`], which the value codecs use to escape every leaf.
10//! [`Display`](core::fmt::Display) for [`Ical`] renders a decoded calendar
11//! straight to its serialized bytes through here.
12
13use core::fmt;
14
15use alloc::{borrow::Cow, boxed::Box, string::ToString, vec, vec::Vec};
16
17use crate::{
18    component::IcalComponent,
19    ical::Ical,
20    param::IcalParam,
21    prop::IcalProp,
22    tree::{
23        codec::{Codec, escape::escape_with, mode::Escaper},
24        cst::{IcalCst, IcalItem},
25        leaf::{IcalLeaf, IcalValueLeaf},
26        line::IcalLine,
27        param::node::IcalParamNode,
28        value::node::IcalValueNode,
29        wire::IcalWire,
30    },
31};
32
33impl Ical<'_> {
34    /// Encode the whole calendar into a `VCALENDAR` CST for its version's
35    /// escaping mode. `VERSION` is emitted as the first property.
36    pub fn encode(&self) -> IcalCst<'static> {
37        let escaper = Escaper::for_version(self.version);
38
39        let mut items = Vec::with_capacity(1 + self.props.len() + self.components.len());
40        items.push(IcalItem::Prop(IcalLine::text(
41            "VERSION",
42            self.version.to_string(),
43        )));
44        items.extend(
45            self.props
46                .iter()
47                .map(|prop| IcalItem::Prop(prop.encode(escaper))),
48        );
49        items.extend(
50            self.components
51                .iter()
52                .map(|component| IcalItem::Component(Box::new(component.encode(escaper)))),
53        );
54
55        IcalCst {
56            begin: Some(IcalLine::text("BEGIN", "VCALENDAR")),
57            items,
58            end: Some(IcalLine::text("END", "VCALENDAR")),
59            trailing: Cow::Borrowed(""),
60        }
61    }
62}
63
64impl IcalComponent<'_> {
65    /// Encode this component (and its nested components) into a CST for the
66    /// given escaping mode.
67    pub fn encode(&self, escaper: Escaper) -> IcalCst<'static> {
68        let name = self.name.to_string();
69
70        let mut items = Vec::with_capacity(self.props.len() + self.components.len());
71        items.extend(
72            self.props
73                .iter()
74                .map(|prop| IcalItem::Prop(prop.encode(escaper))),
75        );
76        items.extend(
77            self.components
78                .iter()
79                .map(|component| IcalItem::Component(Box::new(component.encode(escaper)))),
80        );
81
82        IcalCst {
83            begin: Some(IcalLine::text("BEGIN", name.clone())),
84            items,
85            end: Some(IcalLine::text("END", name)),
86            trailing: Cow::Borrowed(""),
87        }
88    }
89}
90
91impl<'a> From<Ical<'a>> for IcalCst<'static> {
92    fn from(cal: Ical<'a>) -> Self {
93        cal.encode()
94    }
95}
96
97impl IcalProp<'_> {
98    /// Encode the property into a raw content line for the given escaping mode,
99    /// dispatching on its value.
100    pub fn encode(&self, escaper: Escaper) -> IcalLine<'static> {
101        IcalLine {
102            name: IcalLeaf::from(self.name.to_string()),
103            params: self.params.iter().map(IcalParam::encode).collect(),
104            value: self.value.encode(escaper),
105            eol: IcalLeaf::from("\r\n".to_string()),
106            // NOTE: An encoded property has no wire history: it is written out
107            // unfolded, in canonical form.
108            wire: IcalWire::default(),
109        }
110    }
111}
112
113impl IcalParam<'_> {
114    /// Encode the parameter into a raw parameter node, dispatching on its kind.
115    pub fn encode(&self) -> IcalParamNode<'static> {
116        use crate::param::IcalParamKind::*;
117
118        match self {
119            IcalParam::AltRep(v) => param_scalar(&AltRep, v),
120            IcalParam::Cn(v) => param_scalar(&Cn, v),
121            IcalParam::CuType(v) => param_scalar(&CuType, v),
122            IcalParam::DelegatedFrom(vs) => param_list(&DelegatedFrom, vs),
123            IcalParam::DelegatedTo(vs) => param_list(&DelegatedTo, vs),
124            IcalParam::Dir(v) => param_scalar(&Dir, v),
125            IcalParam::Encoding(v) => param_scalar(&Encoding, v),
126            IcalParam::FmtType(v) => param_scalar(&FmtType, v),
127            IcalParam::FbType(v) => param_scalar(&FbType, v),
128            IcalParam::Language(v) => param_scalar(&Language, v),
129            IcalParam::Member(vs) => param_list(&Member, vs),
130            IcalParam::PartStat(v) => param_scalar(&PartStat, v),
131            IcalParam::Range(v) => param_scalar(&Range, v),
132            IcalParam::Related(v) => param_scalar(&Related, v),
133            IcalParam::RelType(v) => param_scalar(&RelType, v),
134            IcalParam::Role(v) => param_scalar(&Role, v),
135            IcalParam::Rsvp(v) => param_scalar(&Rsvp, v),
136            IcalParam::SentBy(v) => param_scalar(&SentBy, v),
137            IcalParam::TzId(v) => param_scalar(&TzId, v),
138            IcalParam::Value(v) => param_scalar(&Value, v),
139            IcalParam::Display(v) => param_scalar(&Display, v),
140            IcalParam::Email(v) => param_scalar(&Email, v),
141            IcalParam::Feature(vs) => param_list(&Feature, vs),
142            IcalParam::Label(v) => param_scalar(&Label, v),
143            IcalParam::Order(v) => param_scalar(&Order, v),
144            IcalParam::Schema(v) => param_scalar(&Schema, v),
145            IcalParam::Derived(v) => param_scalar(&Derived, v),
146            IcalParam::ScheduleAgent(v) => param_scalar(&ScheduleAgent, v),
147            IcalParam::ScheduleForceSend(v) => param_scalar(&ScheduleForceSend, v),
148            IcalParam::ScheduleStatus(v) => param_scalar(&ScheduleStatus, v),
149            IcalParam::LinkRel(v) => param_scalar(&LinkRel, v),
150            IcalParam::Gap(v) => param_scalar(&Gap, v),
151            IcalParam::Charset(v) => param_scalar(&Charset, v),
152
153            IcalParam::Unknown { name, values } => IcalParamNode {
154                name: IcalLeaf::from(name.to_string()),
155                values: values
156                    .iter()
157                    .map(|v| IcalLeaf::from(v.to_string()))
158                    .collect(),
159            },
160        }
161    }
162}
163
164/// Serialize the decoded calendar by encoding it into a CST (canonical).
165impl fmt::Display for Ical<'_> {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        write!(f, "{}", self.encode())
168    }
169}
170
171/// A one-component, one-value syntax node, escaping the value by the given
172/// mode.
173pub(crate) fn scalar_node(value: &str, escaper: Escaper) -> IcalValueNode<'static> {
174    IcalValueNode::from_components(vec![encode_component(&[value], escaper)], escaper)
175}
176
177/// Escape and own a clean value list into one component, by escaping mode.
178pub(crate) fn encode_component<S: AsRef<str>>(
179    values: &[S],
180    escaper: Escaper,
181) -> Vec<IcalValueLeaf<'static>> {
182    values
183        .iter()
184        .map(|v| IcalValueLeaf::from(escape_with(v.as_ref().as_bytes(), escaper).into_owned()))
185        .collect()
186}
187
188/// A parameter node from a single value (parameter values are not escaped: the
189/// wire form is quoted, not backslash-escaped).
190fn param_scalar(name: &str, value: &str) -> IcalParamNode<'static> {
191    IcalParamNode {
192        name: IcalLeaf::from(name.to_string()),
193        values: vec![IcalLeaf::from(value.to_string())],
194    }
195}
196
197/// A parameter node from a value list (parameter values are not escaped).
198fn param_list(name: &str, values: &[Cow<'_, str>]) -> IcalParamNode<'static> {
199    IcalParamNode {
200        name: IcalLeaf::from(name.to_string()),
201        values: values
202            .iter()
203            .map(|v| IcalLeaf::from(v.to_string()))
204            .collect(),
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use alloc::{borrow::Cow, string::ToString};
211
212    use crate::{
213        tree::{
214            codec::{Codec, mode::Escaper},
215            cst::IcalCst,
216        },
217        value::text::IcalText,
218    };
219
220    #[test]
221    fn encodes_a_text_value_escaping_it() {
222        let node = IcalText(Cow::Borrowed("hi, there")).encode(Escaper::Modern);
223        assert_eq!(node.to_string(), r"hi\, there");
224    }
225
226    #[test]
227    fn round_trips_a_decoded_calendar_back_to_bytes() {
228        let input = concat!(
229            "BEGIN:VCALENDAR\r\n",
230            "VERSION:2.0\r\n",
231            "PRODID:-//x//EN\r\n",
232            "BEGIN:VEVENT\r\n",
233            "UID:1\r\n",
234            "DTSTAMP:20260101T000000Z\r\n",
235            "SUMMARY:Lunch\r\n",
236            "END:VEVENT\r\n",
237            "END:VCALENDAR\r\n",
238        );
239        let cst = IcalCst::parse(input).unwrap();
240        let cal = cst.decode();
241        assert_eq!(cal.to_string(), input);
242    }
243}