ical/tree/value/
cursor.rs1use alloc::{borrow::Cow, vec::Vec};
21
22use crate::tree::{line::IcalLine, param::lens::IcalParamLens};
23
24pub struct IcalValueCursor<'c, 'a> {
27 pub line: &'c mut IcalLine<'a>,
29}
30
31impl IcalValueCursor<'_, '_> {
32 pub fn text(&self) -> Cow<'_, str> {
34 self.line.value.decode_scalar_at(0)
35 }
36
37 pub fn set_text(&mut self, value: impl AsRef<str>) {
41 self.line.value.set_at(0, &[value]);
42 }
43
44 pub fn bytes(&self) -> Cow<'_, [u8]> {
50 self.line.value.decode_bytes_at(0)
51 }
52
53 pub fn set_bytes(&mut self, value: impl AsRef<[u8]>) {
58 self.line.value.set_bytes_at(0, &[value]);
59 }
60
61 #[cfg(feature = "quoted-printable")]
67 pub fn quoted_printable(&self) -> Vec<u8> {
68 let raw = self.bytes();
69
70 if self.line.is_quoted_printable() {
71 quoted_printable::decode(raw.as_ref(), quoted_printable::ParseMode::Robust)
72 .unwrap_or_else(|_| raw.into_owned())
73 } else {
74 raw.into_owned()
75 }
76 }
77
78 #[cfg(feature = "encoding")]
83 pub fn charset(&self) -> alloc::string::String {
84 #[cfg(feature = "quoted-printable")]
85 let bytes = self.quoted_printable();
86 #[cfg(not(feature = "quoted-printable"))]
87 let bytes = self.bytes().into_owned();
88
89 let encoding = self
90 .line
91 .charset_label()
92 .and_then(|label| encoding_rs::Encoding::for_label(label.as_bytes()))
93 .unwrap_or(encoding_rs::UTF_8);
94
95 encoding.decode_without_bom_handling(&bytes).0.into_owned()
96 }
97
98 pub fn list(&self) -> Vec<Cow<'_, str>> {
101 self.line.value.decode_at(0)
102 }
103
104 pub fn set_list<S: AsRef<str>>(&mut self, values: &[S]) {
106 self.line.value.set_at(0, values);
107 }
108
109 pub fn component(&self, i: usize) -> Vec<Cow<'_, str>> {
111 self.line.value.decode_at(i)
112 }
113
114 pub fn set_component<S: AsRef<str>>(&mut self, i: usize, values: &[S]) {
116 self.line.value.set_at(i, values);
117 }
118
119 pub fn param<P: IcalParamLens>(&self) -> Option<P::Target<'_>> {
121 self.line.param::<P>()
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use alloc::string::ToString;
128
129 use crate::tree::{cst::IcalCst, prop::summary::SUMMARY};
130
131 const HEAD: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//x//EN\r\n";
132 const TAIL: &str = "END:VCALENDAR\r\n";
133
134 fn cal(prop_line: &str) -> alloc::string::String {
135 alloc::format!("{HEAD}{prop_line}\r\n{TAIL}")
136 }
137
138 #[test]
139 fn edits_a_scalar_value_in_place_escaping_it() {
140 let raw = cal("SUMMARY:Lunch");
141 let mut c = IcalCst::parse(&raw).unwrap();
142 c.prop_mut::<SUMMARY>().unwrap().set_text("Tea, now");
143 assert!(c.to_string().contains("SUMMARY:Tea\\, now\r\n"));
144 }
145
146 #[test]
147 fn writes_and_reads_a_foreign_charset_value_as_raw_bytes() {
148 use crate::tree::prop::description::DESCRIPTION;
149
150 let raw = cal("DESCRIPTION;CHARSET=ISO-8859-1:x");
151 let mut c = IcalCst::parse(&raw).unwrap();
152
153 let latin1 = [b'c', b'a', b'f', 0xE9];
155 c.prop_mut::<DESCRIPTION>().unwrap().set_bytes(latin1);
156
157 assert_eq!(
158 c.prop_mut::<DESCRIPTION>().unwrap().bytes().as_ref(),
159 &latin1,
160 );
161 assert!(c.to_bytes().windows(4).any(|window| window == latin1));
162 }
163
164 #[cfg(feature = "quoted-printable")]
165 #[test]
166 fn quoted_printable_helper_resolves_octets() {
167 use crate::tree::prop::description::DESCRIPTION;
168
169 let raw = cal("DESCRIPTION;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:caf=E9");
170 let mut c = IcalCst::parse(&raw).unwrap();
171
172 assert_eq!(
173 c.prop_mut::<DESCRIPTION>().unwrap().quoted_printable(),
174 [b'c', b'a', b'f', 0xE9],
175 );
176 }
177
178 #[cfg(all(feature = "encoding", feature = "quoted-printable"))]
179 #[test]
180 fn charset_helper_transcodes_to_utf8() {
181 use crate::tree::prop::description::DESCRIPTION;
182
183 let raw = cal("DESCRIPTION;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:caf=E9");
184 let mut c = IcalCst::parse(&raw).unwrap();
185
186 assert_eq!(c.prop_mut::<DESCRIPTION>().unwrap().charset(), "café");
187 }
188
189 #[test]
190 fn edits_one_structured_component_preserving_the_rest() {
191 use crate::tree::prop::geo::GEO;
192
193 let raw = cal("GEO:37.0;-122.0");
194 let mut c = IcalCst::parse(&raw).unwrap();
195 c.prop_mut::<GEO>().unwrap().set_component(1, &["-100.0"]);
196 assert!(c.to_string().contains("GEO:37.0;-100.0\r\n"));
197 }
198
199 #[test]
200 fn exercises_every_generic_accessor() {
201 use crate::tree::prop::categories::CATEGORIES;
202
203 let raw = cal("CATEGORIES:a,b");
204 let mut c = IcalCst::parse(&raw).unwrap();
205 let mut cursor = c.prop_mut::<CATEGORIES>().unwrap();
206
207 let _ = cursor.text();
208 let _ = cursor.list();
209 let _ = cursor.component(0);
210 cursor.set_text("x");
211 cursor.set_list(&["a", "b"]);
212 cursor.set_component(1, &["y"]);
213 }
214}