1use 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 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 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 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 wire: IcalWire::default(),
109 }
110 }
111}
112
113impl IcalParam<'_> {
114 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
164impl fmt::Display for Ical<'_> {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 write!(f, "{}", self.encode())
168 }
169}
170
171pub(crate) fn scalar_node(value: &str, escaper: Escaper) -> IcalValueNode<'static> {
174 IcalValueNode::from_components(vec![encode_component(&[value], escaper)], escaper)
175}
176
177pub(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
188fn 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
197fn 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}