Skip to main content

vcard/tree/codec/
encode.rs

1//! # Encode (model to syntax)
2//!
3//! The write side of the structural bridge: project the decoded model onto a raw
4//! syntax tree. A value's [`VcardCodec`] impl encodes it into a
5//! [`VcardValueNode`], a [`VcardParam`] into a [`VcardParamNode`], a
6//! [`VcardProp`] into a [`VcardLine`] (its name taken verbatim from the
7//! property, its value delegated to the value codec), and a [`Vcard`] into a
8//! whole [`VcardCst`].
9//!
10//! The card is encoded for its version's [`VcardEscaper`], which the value
11//! codecs use to escape every leaf (through the sibling
12//! [`escape`](crate::tree::codec::escape) codec) and to pick any
13//! version-specific value shape; byte-preserving edits are the cursors' job, not
14//! this module's. [`Display`](core::fmt::Display) for [`Vcard`] renders a
15//! decoded card straight to its serialized bytes through here.
16
17use core::fmt;
18
19use alloc::{borrow::Cow, string::ToString, vec, vec::Vec};
20
21use crate::{
22    param::VcardParam,
23    prop::VcardProp,
24    tree::{
25        codec::{VcardCodec, escape::escape_with, mode::VcardEscaper},
26        cst::VcardCst,
27        leaf::{VcardLeaf, VcardValueLeaf},
28        line::VcardLine,
29        param::node::VcardParamNode,
30        value::node::VcardValueNode,
31    },
32    vcard::Vcard,
33};
34
35impl Vcard<'_> {
36    /// Encode the whole card into a CST for its version's escaping mode.
37    pub fn encode(&self) -> VcardCst<'static> {
38        let escaper = VcardEscaper::for_version(self.version);
39
40        let mut cst = VcardCst::v4();
41        // NOTE: v4() seeds a VERSION line as the first property; set it to this
42        // card's version, then append the rest. VERSION stays an ordinary
43        // property.
44        cst.props[0] = VcardLine::text("VERSION", self.version.to_string());
45        cst.props
46            .extend(self.properties.iter().map(|prop| prop.encode(escaper)));
47
48        cst
49    }
50}
51
52impl<'a> From<Vcard<'a>> for VcardCst<'static> {
53    fn from(card: Vcard<'a>) -> Self {
54        card.encode()
55    }
56}
57
58impl VcardProp<'_> {
59    /// Encode the property into a raw content line for the given escaping mode,
60    /// dispatching on its value.
61    pub fn encode(&self, escaper: VcardEscaper) -> VcardLine<'static> {
62        VcardLine {
63            name: VcardLeaf::from(self.name.to_string()),
64            params: self.params.iter().map(VcardParam::encode).collect(),
65            value: self.value.encode(escaper),
66            eol: VcardLeaf::from("\r\n".to_string()),
67        }
68    }
69}
70
71impl VcardParam<'_> {
72    /// Encode the parameter into a raw parameter node, dispatching on its kind.
73    pub fn encode(&self) -> VcardParamNode<'static> {
74        use crate::param::VcardParamKind::*;
75
76        match self {
77            VcardParam::Language(v) => param_scalar(&Language, v),
78            VcardParam::Charset(v) => param_scalar(&Charset, v),
79            VcardParam::Encoding(v) => param_scalar(&Encoding, v),
80            VcardParam::Value(v) => param_scalar(&Value, v),
81            VcardParam::Pref(v) => param_scalar(&Pref, v),
82            VcardParam::AltId(v) => param_scalar(&AltId, v),
83            VcardParam::Pid(vs) => param_list(&Pid, vs),
84            VcardParam::Type(vs) => param_list(&Type, vs),
85            VcardParam::MediaType(v) => param_scalar(&MediaType, v),
86            VcardParam::CalScale(v) => param_scalar(&CalScale, v),
87            VcardParam::SortAs(vs) => param_list(&SortAs, vs),
88            VcardParam::Geo(v) => param_scalar(&Geo, v),
89            VcardParam::Tz(v) => param_scalar(&Tz, v),
90            VcardParam::Label(v) => param_scalar(&Label, v),
91            VcardParam::Author(v) => param_scalar(&Author, v),
92            VcardParam::AuthorName(v) => param_scalar(&AuthorName, v),
93            VcardParam::Created(v) => param_scalar(&Created, v),
94            VcardParam::Derived(v) => param_scalar(&Derived, v),
95            VcardParam::Jsptr(v) => param_scalar(&Jsptr, v),
96            VcardParam::Phonetic(v) => param_scalar(&Phonetic, v),
97            VcardParam::PropId(v) => param_scalar(&PropId, v),
98            VcardParam::Script(v) => param_scalar(&Script, v),
99            VcardParam::ServiceType(v) => param_scalar(&ServiceType, v),
100            VcardParam::Username(v) => param_scalar(&Username, v),
101
102            VcardParam::Unknown { name, values } => VcardParamNode {
103                name: VcardLeaf::from(name.to_string()),
104                values: values
105                    .iter()
106                    .map(|v| VcardLeaf::from(v.to_string()))
107                    .collect(),
108            },
109        }
110    }
111}
112
113/// Serialize the decoded card by encoding it into a CST (canonical).
114impl fmt::Display for Vcard<'_> {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}", self.encode())
117    }
118}
119
120/// A one-component, one-value syntax node, escaping the value by the given
121/// mode.
122pub(crate) fn scalar_node(value: &str, escaper: VcardEscaper) -> VcardValueNode<'static> {
123    VcardValueNode::from_components(vec![encode_component(&[value], escaper)], escaper)
124}
125
126/// Escape and own a clean value list into one component, by escaping mode.
127pub(crate) fn encode_component<S: AsRef<str>>(
128    values: &[S],
129    escaper: VcardEscaper,
130) -> Vec<VcardValueLeaf<'static>> {
131    values.iter().map(|v| encode_leaf(v, escaper)).collect()
132}
133
134/// Escape one value into an owned leaf, by escaping mode. Backs the per-item
135/// value edits, which splice a single leaf and leave its siblings' bytes as
136/// they were parsed.
137pub(crate) fn encode_leaf<S: AsRef<str>>(
138    value: S,
139    escaper: VcardEscaper,
140) -> VcardValueLeaf<'static> {
141    VcardValueLeaf::from(escape_with(value.as_ref().as_bytes(), escaper).into_owned())
142}
143
144/// A parameter node from a single value (parameter values are not escaped: the
145/// wire form is quoted, not backslash-escaped).
146fn param_scalar(name: &str, value: &str) -> VcardParamNode<'static> {
147    VcardParamNode {
148        name: VcardLeaf::from(name.to_string()),
149        values: vec![VcardLeaf::from(value.to_string())],
150    }
151}
152
153/// A parameter node from a value list (parameter values are not escaped).
154fn param_list(name: &str, values: &[Cow<'_, str>]) -> VcardParamNode<'static> {
155    VcardParamNode {
156        name: VcardLeaf::from(name.to_string()),
157        values: values
158            .iter()
159            .map(|v| VcardLeaf::from(v.to_string()))
160            .collect(),
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use alloc::{borrow::Cow, string::ToString, vec};
167
168    use crate::{
169        tree::{
170            codec::{VcardCodec, mode::VcardEscaper},
171            cst::VcardCst,
172        },
173        value::{n::VcardN, text::VcardText},
174    };
175
176    #[test]
177    fn encodes_a_text_value_escaping_it() {
178        let node = VcardText(Cow::Borrowed("hi, there")).encode(VcardEscaper::Modern);
179        assert_eq!(node.to_string(), r"hi\, there");
180    }
181
182    #[test]
183    fn encodes_the_structured_n_value_with_all_components() {
184        let n = VcardN {
185            family: vec![Cow::Borrowed("Doe")],
186            ..Default::default()
187        };
188        assert_eq!(n.encode(VcardEscaper::Modern).to_string(), "Doe;;;;");
189    }
190
191    #[test]
192    fn encodes_the_geo_pair_in_the_cards_own_version() {
193        // NOTE: A 2.1 GEO decodes to a coordinate pair; re-encoding for 2.1
194        // must write it back as a comma pair, not the 3.0 semicolon form.
195        let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:2.1\r\nGEO:37.0,-122.0\r\nEND:VCARD\r\n")
196            .unwrap();
197        let card = cst.decode();
198        assert!(
199            card.to_string().contains("GEO:37.0,-122.0\r\n"),
200            "{}",
201            card.to_string(),
202        );
203
204        // NOTE: The same pair round-trips through 3.0 with a semicolon.
205        let cst = VcardCst::parse("BEGIN:VCARD\r\nVERSION:3.0\r\nGEO:37.0;-122.0\r\nEND:VCARD\r\n")
206            .unwrap();
207        let card = cst.decode();
208        assert!(
209            card.to_string().contains("GEO:37.0;-122.0\r\n"),
210            "{}",
211            card.to_string(),
212        );
213    }
214}