1use core::fmt;
21
22use alloc::{borrow::Cow, string::ToString, vec, vec::Vec};
23
24use crate::{
25 param::VcardParam,
26 prop::VcardProp,
27 tree::{
28 codec::{
29 VcardCodec,
30 escape::{escape_param, escape_with},
31 mode::VcardEscaper,
32 },
33 cst::VcardCst,
34 leaf::{VcardLeaf, VcardValueLeaf},
35 line::VcardLine,
36 param::node::VcardParamNode,
37 value::node::VcardValueNode,
38 wire::VcardWire,
39 },
40 validator::VcardValid,
41 vcard::Vcard,
42};
43
44impl Vcard<'_> {
45 pub fn encode(&self) -> VcardCst<'static> {
47 let escaper = VcardEscaper::for_version(self.version);
48
49 let mut cst = VcardCst::v4();
50 cst.props[0] = VcardLine::text("VERSION", self.version.to_string());
54 cst.props
55 .extend(self.properties.iter().map(|prop| prop.encode(escaper)));
56
57 cst
58 }
59}
60
61impl<'a> From<Vcard<'a>> for VcardCst<'static> {
62 fn from(card: Vcard<'a>) -> Self {
63 card.encode()
64 }
65}
66
67impl From<VcardValid<Vcard<'_>>> for VcardCst<'static> {
68 fn from(card: VcardValid<Vcard<'_>>) -> Self {
69 card.into_inner().encode()
70 }
71}
72
73impl VcardProp<'_> {
74 pub fn encode(&self, escaper: VcardEscaper) -> VcardLine<'static> {
77 VcardLine {
78 name: VcardLeaf::from(self.name.to_string()),
79 params: self
80 .params
81 .iter()
82 .map(|param| param.encode(escaper))
83 .collect(),
84 value: self.value.encode(escaper),
85 eol: VcardLeaf::from("\r\n".to_string()),
86 wire: VcardWire::default(),
87 }
88 }
89}
90
91impl VcardParam<'_> {
92 pub fn encode(&self, escaper: VcardEscaper) -> VcardParamNode<'static> {
95 use crate::param::VcardParamKind::*;
96
97 match self {
98 VcardParam::Language(v) => param_scalar(&Language, v, escaper),
99 VcardParam::Charset(v) => param_scalar(&Charset, v, escaper),
100 VcardParam::Encoding(v) => param_scalar(&Encoding, v, escaper),
101 VcardParam::Value(v) => param_scalar(&Value, v, escaper),
102 VcardParam::Pref(v) => param_scalar(&Pref, v, escaper),
103 VcardParam::AltId(v) => param_scalar(&AltId, v, escaper),
104 VcardParam::Pid(vs) => param_list(&Pid, vs, escaper),
105 VcardParam::Type(vs) => param_list(&Type, vs, escaper),
106 VcardParam::MediaType(v) => param_scalar(&MediaType, v, escaper),
107 VcardParam::CalScale(v) => param_scalar(&CalScale, v, escaper),
108 VcardParam::SortAs(vs) => param_list(&SortAs, vs, escaper),
109 VcardParam::Geo(v) => param_scalar(&Geo, v, escaper),
110 VcardParam::Tz(v) => param_scalar(&Tz, v, escaper),
111 VcardParam::Label(v) => param_scalar(&Label, v, escaper),
112 VcardParam::Author(v) => param_scalar(&Author, v, escaper),
113 VcardParam::AuthorName(v) => param_scalar(&AuthorName, v, escaper),
114 VcardParam::Created(v) => param_scalar(&Created, v, escaper),
115 VcardParam::Derived(v) => param_scalar(&Derived, v, escaper),
116 VcardParam::Jsptr(v) => param_scalar(&Jsptr, v, escaper),
117 VcardParam::Phonetic(v) => param_scalar(&Phonetic, v, escaper),
118 VcardParam::PropId(v) => param_scalar(&PropId, v, escaper),
119 VcardParam::Script(v) => param_scalar(&Script, v, escaper),
120 VcardParam::ServiceType(v) => param_scalar(&ServiceType, v, escaper),
121 VcardParam::Username(v) => param_scalar(&Username, v, escaper),
122
123 VcardParam::Unknown { name, values } => param_list(name, values, escaper),
124 }
125 }
126}
127
128impl fmt::Display for Vcard<'_> {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}", self.encode())
132 }
133}
134
135pub(crate) fn scalar_node(value: &str, escaper: VcardEscaper) -> VcardValueNode<'static> {
138 VcardValueNode::from_components(vec![encode_component(&[value], escaper)], escaper)
139}
140
141pub(crate) fn verbatim_node(value: &str, escaper: VcardEscaper) -> VcardValueNode<'static> {
147 VcardValueNode::from_raw(value.as_bytes().to_vec(), escaper)
148}
149
150pub(crate) fn encode_component<S: AsRef<str>>(
152 values: &[S],
153 escaper: VcardEscaper,
154) -> Vec<VcardValueLeaf<'static>> {
155 values.iter().map(|v| encode_leaf(v, escaper)).collect()
156}
157
158pub(crate) fn encode_bytes_component<B: AsRef<[u8]>>(
163 values: &[B],
164 escaper: VcardEscaper,
165) -> Vec<VcardValueLeaf<'static>> {
166 values
167 .iter()
168 .map(|v| VcardValueLeaf::from(escape_with(v.as_ref(), escaper).into_owned()))
169 .collect()
170}
171
172pub(crate) fn encode_leaf<S: AsRef<str>>(
176 value: S,
177 escaper: VcardEscaper,
178) -> VcardValueLeaf<'static> {
179 VcardValueLeaf::from(escape_with(value.as_ref().as_bytes(), escaper).into_owned())
180}
181
182fn param_scalar(name: &str, value: &str, escaper: VcardEscaper) -> VcardParamNode<'static> {
185 VcardParamNode {
186 name: VcardLeaf::from(name.to_string()),
187 values: vec![VcardLeaf::from(escape_param(value, escaper).into_owned())],
188 escaper,
189 }
190}
191
192fn param_list(
195 name: &str,
196 values: &[Cow<'_, str>],
197 escaper: VcardEscaper,
198) -> VcardParamNode<'static> {
199 VcardParamNode {
200 name: VcardLeaf::from(name.to_string()),
201 values: values
202 .iter()
203 .map(|v| VcardLeaf::from(escape_param(v, escaper).into_owned()))
204 .collect(),
205 escaper,
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use alloc::{borrow::Cow, string::ToString, vec};
212
213 use crate::{
214 param::VcardParam,
215 tree::{
216 codec::{VcardCodec, mode::VcardEscaper},
217 cst::VcardCst,
218 },
219 value::{n::VcardN, text::VcardText},
220 };
221
222 #[test]
223 fn encodes_a_text_value_escaping_it() {
224 let node = VcardText(Cow::Borrowed("hi, there")).encode(VcardEscaper::V4_0);
225 assert_eq!(node.to_string(), r"hi\, there");
226 }
227
228 #[test]
229 fn encodes_the_structured_n_value_with_all_components() {
230 let n = VcardN {
231 family: vec![Cow::Borrowed("Doe")],
232 ..Default::default()
233 };
234 assert_eq!(n.encode(VcardEscaper::V4_0).to_string(), "Doe;;;;");
235 }
236
237 #[test]
241 fn encodes_the_geo_pair_in_the_cards_own_version() {
242 let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:2.1\r\nGEO:37.0,-122.0\r\nEND:VCARD\r\n")
243 .unwrap();
244 let card = cst.decode();
245 assert!(
246 card.to_string().contains("GEO:37.0,-122.0\r\n"),
247 "{}",
248 card.to_string(),
249 );
250
251 let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:3.0\r\nGEO:37.0;-122.0\r\nEND:VCARD\r\n")
252 .unwrap();
253 let card = cst.decode();
254 assert!(
255 card.to_string().contains("GEO:37.0;-122.0\r\n"),
256 "{}",
257 card.to_string(),
258 );
259 }
260
261 #[test]
264 fn encodes_the_rfc_6868_parameter_sequences() {
265 let param = VcardParam::Label(Cow::Borrowed("a\nb^c\"d"));
266
267 assert_eq!(
268 param.encode(VcardEscaper::V4_0).to_string(),
269 "LABEL=a^nb^^c^'d",
270 );
271 }
272
273 #[test]
277 fn quotes_a_parameter_value_carrying_a_delimiter() {
278 let param = VcardParam::Geo(Cow::Borrowed("geo:37.386,-122.083"));
279
280 assert_eq!(
281 param.encode(VcardEscaper::V4_0).to_string(),
282 "GEO=\"geo:37.386,-122.083\"",
283 );
284 }
285
286 #[test]
287 fn writes_a_pre_4_0_parameter_unencoded() {
288 let param = VcardParam::Label(Cow::Borrowed("a^b"));
291
292 assert_eq!(param.encode(VcardEscaper::V3_0).to_string(), "LABEL=a^b");
293 }
294
295 #[test]
296 fn round_trips_a_parameter_byte_for_byte() {
297 let input = concat!(
298 "BEGIN:VCARD\r\n",
299 "VERSION:4.0\r\n",
300 "FN;LANGUAGE=en;GEO=\"geo:37.386,-122.083\"",
301 ";X-PATH=\"C:\\temp\";X-NOTE=a^nb^^c^'d:Ada\r\n",
302 "END:VCARD\r\n",
303 );
304 let cst = VcardCst::parse(input).unwrap();
305
306 assert_eq!(cst.decode().to_string(), input);
307 }
308}