use alloc::{borrow::Cow, vec::Vec};
use crate::tree::{line::VcardLine, param::lens::VcardParamLens, value::node::VcardValueNode};
pub struct VcardValueCursor<'c, 'a> {
pub line: &'c mut VcardLine<'a>,
}
impl<'a> VcardValueCursor<'_, 'a> {
pub fn text(&self) -> Cow<'_, str> {
self.line.value.decode_scalar_at(0)
}
pub fn set_text(&mut self, value: impl AsRef<str>) {
self.line.value.set_at(0, &[value]);
}
pub fn bytes(&self) -> Cow<'_, [u8]> {
self.line.value.decode_bytes_at(0)
}
pub fn set_bytes(&mut self, value: impl AsRef<[u8]>) {
self.line.value.set_bytes_at(0, &[value]);
}
#[cfg(feature = "quoted-printable")]
pub fn quoted_printable(&self) -> Vec<u8> {
let raw = self.bytes();
if self.line.is_quoted_printable() {
quoted_printable::decode(raw.as_ref(), quoted_printable::ParseMode::Robust)
.unwrap_or_else(|_| raw.into_owned())
} else {
raw.into_owned()
}
}
#[cfg(feature = "encoding")]
pub fn charset(&self) -> alloc::string::String {
#[cfg(feature = "quoted-printable")]
let bytes = self.quoted_printable();
#[cfg(not(feature = "quoted-printable"))]
let bytes = self.bytes().into_owned();
let encoding = self
.line
.charset_label()
.and_then(|label| encoding_rs::Encoding::for_label(label.as_bytes()))
.unwrap_or(encoding_rs::UTF_8);
encoding.decode_without_bom_handling(&bytes).0.into_owned()
}
pub fn list(&self) -> Vec<Cow<'_, str>> {
self.line.value.decode_at(0)
}
pub fn set_list<S: AsRef<str>>(&mut self, values: &[S]) {
self.line.value.set_at(0, values);
}
pub fn component(&self, i: usize) -> Vec<Cow<'_, str>> {
self.line.value.decode_at(i)
}
pub fn set_component<S: AsRef<str>>(&mut self, i: usize, values: &[S]) {
self.line.value.set_at(i, values);
}
pub fn list_at(&mut self, i: usize) -> VcardListCursor<'_, 'a> {
VcardListCursor {
node: &mut self.line.value,
component: i,
}
}
pub fn list_mut(&mut self) -> VcardListCursor<'_, 'a> {
self.list_at(0)
}
pub fn param<P: VcardParamLens>(&self) -> Option<P::Target<'_>> {
self.line.param::<P>()
}
}
pub struct VcardListCursor<'c, 'a> {
node: &'c mut VcardValueNode<'a>,
component: usize,
}
impl VcardListCursor<'_, '_> {
pub fn len(&self) -> usize {
self.node.value_count(self.component)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self, j: usize) -> Option<Cow<'_, str>> {
self.node.decode_at(self.component).into_iter().nth(j)
}
pub fn set<S: AsRef<str>>(&mut self, j: usize, value: S) -> &mut Self {
self.node.set_value_at(self.component, j, value);
self
}
pub fn insert<S: AsRef<str>>(&mut self, j: usize, value: S) -> &mut Self {
self.node.insert_value_at(self.component, j, value);
self
}
pub fn push<S: AsRef<str>>(&mut self, value: S) -> &mut Self {
self.node.push_value(self.component, value);
self
}
pub fn remove(&mut self, j: usize) -> &mut Self {
self.node.remove_value_at(self.component, j);
self
}
}
#[cfg(test)]
mod tests {
use alloc::{string::ToString, vec};
use crate::tree::{
cst::VcardCst,
prop::{adr::ADR, r#fn::FN},
};
#[test]
fn edits_a_scalar_value_in_place_escaping_it() {
let mut card =
VcardCst::parse("BEGIN:VCARD\r\nVERSION:4.0\r\nFN:John\r\nEND:VCARD\r\n").unwrap();
card.prop_mut::<FN>().unwrap().set_text("Jane, Q");
assert!(card.to_string().contains("FN:Jane\\, Q\r\n"));
}
#[test]
fn writes_and_reads_a_foreign_charset_value_as_raw_bytes() {
use crate::tree::prop::note::NOTE;
let mut card = VcardCst::parse(
"BEGIN:VCARD\r\nVERSION:2.1\r\nNOTE;CHARSET=ISO-8859-1:x\r\nEND:VCARD\r\n",
)
.unwrap();
let latin1 = [b'c', b'a', b'f', 0xE9];
card.prop_mut::<NOTE>().unwrap().set_bytes(latin1);
assert_eq!(card.prop_mut::<NOTE>().unwrap().bytes().as_ref(), &latin1);
assert!(card.to_bytes().windows(4).any(|window| window == latin1));
}
#[test]
fn bytes_returns_the_raw_undecoded_value() {
use crate::tree::prop::note::NOTE;
let mut card = VcardCst::parse(concat!(
"BEGIN:VCARD\r\n",
"VERSION:2.1\r\n",
"NOTE;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:caf=E9\r\n",
"END:VCARD\r\n",
))
.unwrap();
assert_eq!(card.prop_mut::<NOTE>().unwrap().bytes().as_ref(), b"caf=E9");
}
#[cfg(feature = "quoted-printable")]
#[test]
fn quoted_printable_helper_resolves_octets() {
use crate::tree::prop::note::NOTE;
let mut card = VcardCst::parse(concat!(
"BEGIN:VCARD\r\n",
"VERSION:2.1\r\n",
"NOTE;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:caf=E9\r\n",
"END:VCARD\r\n",
))
.unwrap();
assert_eq!(
card.prop_mut::<NOTE>().unwrap().quoted_printable(),
[b'c', b'a', b'f', 0xE9],
);
}
#[cfg(all(feature = "encoding", feature = "quoted-printable"))]
#[test]
fn charset_helper_transcodes_to_utf8() {
use crate::tree::prop::note::NOTE;
let mut card = VcardCst::parse(concat!(
"BEGIN:VCARD\r\n",
"VERSION:2.1\r\n",
"NOTE;CHARSET=ISO-8859-1;ENCODING=QUOTED-PRINTABLE:caf=E9\r\n",
"END:VCARD\r\n",
))
.unwrap();
assert_eq!(card.prop_mut::<NOTE>().unwrap().charset(), "café");
}
#[test]
fn edits_one_structured_component_preserving_the_rest() {
let mut card =
VcardCst::parse("BEGIN:VCARD\r\nVERSION:4.0\r\nADR:;;Old St;;;;\r\nEND:VCARD\r\n")
.unwrap();
card.prop_mut::<ADR>().unwrap().set_street(&["New St"]);
assert!(card.to_string().contains("ADR:;;New St;;;;\r\n"));
}
#[test]
fn walks_a_list_editing_items_without_reformatting_siblings() {
use crate::tree::prop::nickname::NICKNAME;
let mut card = VcardCst::parse(concat!(
"BEGIN:VCARD\r\n",
"VERSION:4.0\r\n",
"NICKNAME:a\\:b,middle,z\r\n",
"END:VCARD\r\n",
))
.unwrap();
card.prop_mut::<NICKNAME>().unwrap().list_mut().remove(1);
let out = card.to_string();
assert!(out.contains("NICKNAME:a\\:b,z\r\n"), "got: {out}");
}
#[test]
fn walks_a_list_setting_inserting_and_pushing() {
use crate::tree::prop::nickname::NICKNAME;
let mut card =
VcardCst::parse("BEGIN:VCARD\r\nVERSION:4.0\r\nNICKNAME:a,b,c\r\nEND:VCARD\r\n")
.unwrap();
{
let mut cursor = card.prop_mut::<NICKNAME>().unwrap();
let mut list = cursor.list_mut();
assert_eq!(list.len(), 3);
assert_eq!(list.get(1).as_deref(), Some("b"));
list.set(1, "B").insert(0, "first").push("last");
}
let out = card.to_string();
assert!(out.contains("NICKNAME:first,a,B,c,last\r\n"), "got: {out}");
}
#[test]
fn exercises_every_generic_accessor() {
use crate::tree::prop::note::NOTE;
let mut card =
VcardCst::parse("BEGIN:VCARD\r\nVERSION:4.0\r\nNOTE:a,b\r\nEND:VCARD\r\n").unwrap();
{
let mut cursor = card.prop_mut::<NOTE>().unwrap();
assert_eq!(cursor.text(), "a");
assert_eq!(cursor.list(), vec!["a", "b"]);
assert_eq!(cursor.component(0), vec!["a", "b"]);
cursor.set_text("x");
assert_eq!(cursor.text(), "x");
cursor.set_list(&["a", "b"]);
assert_eq!(cursor.list(), vec!["a", "b"]);
cursor.set_component(1, &["y"]);
assert_eq!(cursor.component(1), vec!["y"]);
}
assert!(card.to_string().contains("NOTE:a,b;y\r\n"), "got: {card}");
}
}