text.rs
⎇
Raw
1//! Content lines of iCalendar and vCard text (RFC 5545, 3.1; RFC 6350, 3.2),
2//! for edits that must leave every other byte alone.
3
4/// Physical lines joined with their folded continuation lines. A leading
5/// byte order mark is dropped: Outlook and Notepad write one.
6pub(crate) fn logical_lines(text: &str) -> Vec<&str> {
7 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
8 let mut out = Vec::new();
9 let mut start = 0;
10 let mut pos = 0;
11 for line in text.split_inclusive('\n') {
12 if pos > start && !line.starts_with([' ', '\t']) {
13 out.push(&text[start..pos]);
14 start = pos;
15 }
16 pos += line.len();
17 }
18 if pos > start {
19 out.push(&text[start..pos]);
20 }
21 out
22}
23
24pub(crate) fn unfold(line: &str) -> String {
25 line.replace("\r\n ", "")
26 .replace("\r\n\t", "")
27 .replace("\n ", "")
28 .replace("\n\t", "")
29 .trim_end_matches(['\r', '\n'])
30 .to_string()
31}
32
33/// The property name, upper case.
34pub(crate) fn name(line: &str) -> String {
35 let end = line.find([':', ';', '\r', '\n']).unwrap_or(line.len());
36 line[..end].trim().to_ascii_uppercase()
37}
38
39/// Where the value of an unfolded line starts: after the first colon
40/// outside a quoted parameter value.
41pub(crate) fn value_start(line: &str) -> usize {
42 let mut quoted = false;
43 for (i, c) in line.char_indices() {
44 match c {
45 '"' => quoted = !quoted,
46 ':' if !quoted => return i + 1,
47 _ => {}
48 }
49 }
50 line.len()
51}
52
53pub(crate) fn value(line: &str) -> &str {
54 &line[value_start(line)..]
55}
56
57/// The name and parameters of a line, without the `:` before the value.
58pub(crate) fn head(line: &str) -> &str {
59 let start = value_start(line);
60 line[..start].strip_suffix(':').unwrap_or(line)
61}
62
63/// The raw parameters of a line, split at `;` outside quoted values. A
64/// quoted TZID may hold `;` and `:`.
65pub(crate) fn param_parts(line: &str) -> Vec<&str> {
66 let head = head(line);
67 let mut parts = Vec::new();
68 let (mut quoted, mut start) = (false, 0);
69 for (i, c) in head.char_indices() {
70 match c {
71 '"' => quoted = !quoted,
72 ';' if !quoted => {
73 parts.push(&head[start..i]);
74 start = i + 1;
75 }
76 _ => {}
77 }
78 }
79 parts.push(&head[start..]);
80 parts.remove(0);
81 parts
82}
83
84/// The value of parameter `key` of an unfolded line, without quotes.
85pub(crate) fn param(line: &str, key: &str) -> Option<String> {
86 param_parts(line).into_iter().find_map(|p| {
87 let (k, v) = p.split_once('=')?;
88 k.trim()
89 .eq_ignore_ascii_case(key)
90 .then(|| v.trim().trim_matches('"').to_string())
91 })
92}
93
94/// An unfolded line folded at 75 octets (RFC 5545, 3.1), ending in `eol`.
95pub(crate) fn fold(line: &str, eol: &str) -> String {
96 let mut out = String::with_capacity(line.len() + eol.len() * (line.len() / 74 + 1));
97 let mut width = 0;
98 for c in line.chars() {
99 if width + c.len_utf8() > 75 {
100 out.push_str(eol);
101 out.push(' ');
102 width = 1;
103 }
104 out.push(c);
105 width += c.len_utf8();
106 }
107 out + eol
108}
109
110/// RFC 5545, 3.3.11. A TZID property is TEXT, so `Athens\, Bucharest`
111/// there names the TZID parameter `"Athens, Bucharest"`.
112pub(crate) fn unescape_text(s: &str) -> String {
113 let mut out = String::with_capacity(s.len());
114 let mut chars = s.chars();
115 while let Some(c) = chars.next() {
116 match (c, chars.clone().next()) {
117 ('\\', Some(n @ ('\\' | ';' | ',' | 'n' | 'N'))) => {
118 out.push(if n.eq_ignore_ascii_case(&'n') {
119 '\n'
120 } else {
121 n
122 });
123 chars.next();
124 }
125 _ => out.push(c),
126 }
127 }
128 out
129}
130
131/// RFC 5545, 3.3.11.
132pub(crate) fn escape_text(s: &str) -> String {
133 s.replace('\\', "\\\\")
134 .replace(';', "\\;")
135 .replace(',', "\\,")
136 .replace('\n', "\\n")
137 .replace('\r', "")
138 .replace(|c: char| c.is_control() && c != '\t', "")
139}
140