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/// The property name without its Apple group prefix.
40pub(crate) fn prop(line: &str) -> String {
41 let n = name(line);
42 n.rsplit_once('.').map_or(n.clone(), |(_, n)| n.to_string())
43}
44
45/// The Apple group prefix of a property name, like `ITEM1`.
46pub(crate) fn group(line: &str) -> Option<String> {
47 name(line).rsplit_once('.').map(|(g, _)| g.to_string())
48}
49
50/// Where the value of an unfolded line starts: after the first colon
51/// outside a quoted parameter value.
52pub(crate) fn value_start(line: &str) -> usize {
53 let mut quoted = false;
54 for (i, c) in line.char_indices() {
55 match c {
56 '"' => quoted = !quoted,
57 ':' if !quoted => return i + 1,
58 _ => {}
59 }
60 }
61 line.len()
62}
63
64pub(crate) fn value(line: &str) -> &str {
65 &line[value_start(line)..]
66}
67
68/// The name and parameters of a line, without the `:` before the value.
69pub(crate) fn head(line: &str) -> &str {
70 let start = value_start(line);
71 line[..start].strip_suffix(':').unwrap_or(line)
72}
73
74/// The raw parameters of a line, split at `;` outside quoted values. A
75/// quoted TZID may hold `;` and `:`.
76pub(crate) fn param_parts(line: &str) -> Vec<&str> {
77 let head = head(line);
78 let mut parts = Vec::new();
79 let (mut quoted, mut start) = (false, 0);
80 for (i, c) in head.char_indices() {
81 match c {
82 '"' => quoted = !quoted,
83 ';' if !quoted => {
84 parts.push(&head[start..i]);
85 start = i + 1;
86 }
87 _ => {}
88 }
89 }
90 parts.push(&head[start..]);
91 parts.remove(0);
92 parts
93}
94
95/// The value of parameter `key` of an unfolded line, without quotes.
96pub(crate) fn param(line: &str, key: &str) -> Option<String> {
97 param_parts(line).into_iter().find_map(|p| {
98 let (k, v) = p.split_once('=')?;
99 k.trim()
100 .eq_ignore_ascii_case(key)
101 .then(|| v.trim().trim_matches('"').to_string())
102 })
103}
104
105/// An unfolded line folded at 75 octets (RFC 5545, 3.1), ending in `eol`.
106pub(crate) fn fold(line: &str, eol: &str) -> String {
107 let mut out = String::with_capacity(line.len() + eol.len() * (line.len() / 74 + 1));
108 let mut width = 0;
109 for c in line.chars() {
110 if width + c.len_utf8() > 75 {
111 out.push_str(eol);
112 out.push(' ');
113 width = 1;
114 }
115 out.push(c);
116 width += c.len_utf8();
117 }
118 out + eol
119}
120
121/// RFC 5545, 3.3.11. A TZID property is TEXT, so `Athens\, Bucharest`
122/// there names the TZID parameter `"Athens, Bucharest"`.
123pub(crate) fn unescape_text(s: &str) -> String {
124 let mut out = String::with_capacity(s.len());
125 let mut chars = s.chars();
126 while let Some(c) = chars.next() {
127 match (c, chars.clone().next()) {
128 ('\\', Some(n @ ('\\' | ';' | ',' | 'n' | 'N'))) => {
129 out.push(if n.eq_ignore_ascii_case(&'n') {
130 '\n'
131 } else {
132 n
133 });
134 chars.next();
135 }
136 _ => out.push(c),
137 }
138 }
139 out
140}
141
142/// RFC 5545, 3.3.11.
143pub(crate) fn escape_text(s: &str) -> String {
144 s.replace('\\', "\\\\")
145 .replace(';', "\\;")
146 .replace(',', "\\,")
147 .replace('\n', "\\n")
148 .replace('\r', "")
149 .replace(|c: char| c.is_control() && c != '\t', "")
150}
151