//! Content lines of iCalendar and vCard text (RFC 5545, 3.1; RFC 6350, 3.2), //! for edits that must leave every other byte alone. /// Physical lines joined with their folded continuation lines. A leading /// byte order mark is dropped: Outlook and Notepad write one. pub(crate) fn logical_lines(text: &str) -> Vec<&str> { let text = text.strip_prefix('\u{feff}').unwrap_or(text); let mut out = Vec::new(); let mut start = 0; let mut pos = 0; for line in text.split_inclusive('\n') { if pos > start && !line.starts_with([' ', '\t']) { out.push(&text[start..pos]); start = pos; } pos += line.len(); } if pos > start { out.push(&text[start..pos]); } out } pub(crate) fn unfold(line: &str) -> String { line.replace("\r\n ", "") .replace("\r\n\t", "") .replace("\n ", "") .replace("\n\t", "") .trim_end_matches(['\r', '\n']) .to_string() } /// The property name, upper case. pub(crate) fn name(line: &str) -> String { let end = line.find([':', ';', '\r', '\n']).unwrap_or(line.len()); line[..end].trim().to_ascii_uppercase() } /// The property name without its Apple group prefix. pub(crate) fn prop(line: &str) -> String { let n = name(line); n.rsplit_once('.').map_or(n.clone(), |(_, n)| n.to_string()) } /// The Apple group prefix of a property name, like `ITEM1`. pub(crate) fn group(line: &str) -> Option { name(line).rsplit_once('.').map(|(g, _)| g.to_string()) } /// Where the value of an unfolded line starts: after the first colon /// outside a quoted parameter value. pub(crate) fn value_start(line: &str) -> usize { let mut quoted = false; for (i, c) in line.char_indices() { match c { '"' => quoted = !quoted, ':' if !quoted => return i + 1, _ => {} } } line.len() } pub(crate) fn value(line: &str) -> &str { &line[value_start(line)..] } /// The name and parameters of a line, without the `:` before the value. pub(crate) fn head(line: &str) -> &str { let start = value_start(line); line[..start].strip_suffix(':').unwrap_or(line) } /// The raw parameters of a line, split at `;` outside quoted values. A /// quoted TZID may hold `;` and `:`. pub(crate) fn param_parts(line: &str) -> Vec<&str> { split_unquoted(head(line), ';').split_off(1) } /// `s` split at each `sep` outside double quotes. pub(crate) fn split_unquoted(s: &str, sep: char) -> Vec<&str> { let mut out = Vec::new(); let (mut quoted, mut from) = (false, 0); for (i, c) in s.char_indices() { if c == '"' { quoted = !quoted; } else if c == sep && !quoted { out.push(&s[from..i]); from = i + 1; } } out.push(&s[from..]); out } /// The value of parameter `key` of an unfolded line, without quotes. pub(crate) fn param(line: &str, key: &str) -> Option { param_parts(line).into_iter().find_map(|p| { let (k, v) = p.split_once('=')?; k.trim() .eq_ignore_ascii_case(key) .then(|| v.trim().trim_matches('"').to_string()) }) } /// An unfolded line folded at 75 octets (RFC 5545, 3.1), ending in `eol`. pub(crate) fn fold(line: &str, eol: &str) -> String { let mut out = String::with_capacity(line.len() + eol.len() * (line.len() / 74 + 1)); let mut width = 0; for c in line.chars() { if width + c.len_utf8() > 75 { out.push_str(eol); out.push(' '); width = 1; } out.push(c); width += c.len_utf8(); } out + eol } /// RFC 5545, 3.3.11. A TZID property is TEXT, so `Athens\, Bucharest` /// there names the TZID parameter `"Athens, Bucharest"`. pub(crate) fn unescape_text(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut chars = s.chars(); while let Some(c) = chars.next() { match (c, chars.clone().next()) { ('\\', Some(n @ ('\\' | ';' | ',' | 'n' | 'N'))) => { out.push(if n.eq_ignore_ascii_case(&'n') { '\n' } else { n }); chars.next(); } _ => out.push(c), } } out } /// RFC 5545, 3.3.11. pub(crate) fn escape_text(s: &str) -> String { s.replace('\\', "\\\\") .replace(';', "\\;") .replace(',', "\\,") .replace('\n', "\\n") .replace('\r', "") .replace(|c: char| c.is_control() && c != '\t', "") }