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