//! 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. pub(crate) fn logical_lines(text: &str) -> Vec<&str> { 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() } /// 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> { let head = head(line); let mut parts = Vec::new(); let (mut quoted, mut start) = (false, 0); for (i, c) in head.char_indices() { match c { '"' => quoted = !quoted, ';' if !quoted => { parts.push(&head[start..i]); start = i + 1; } _ => {} } } parts.push(&head[start..]); parts.remove(0); parts } /// 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', "") }