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 split_unquoted(head(line), ';').split_off(1)
78}
79
80/// `s` split at each `sep` outside double quotes.
81pub(crate) fn split_unquoted(s: &str, sep: char) -> Vec<&str> {
82 let mut out = Vec::new();
83 let (mut quoted, mut from) = (false, 0);
84 for (i, c) in s.char_indices() {
85 if c == '"' {
86 quoted = !quoted;
87 } else if c == sep && !quoted {
88 out.push(&s[from..i]);
89 from = i + 1;
90 }
91 }
92 out.push(&s[from..]);
93 out
94}
95
96/// The value of parameter `key` of an unfolded line, without quotes.
97pub(crate) fn param(line: &str, key: &str) -> Option<String> {
98 param_parts(line).into_iter().find_map(|p| {
99 let (k, v) = p.split_once('=')?;
100 k.trim()
101 .eq_ignore_ascii_case(key)
102 .then(|| v.trim().trim_matches('"').to_string())
103 })
104}
105
106/// An unfolded line folded at 75 octets (RFC 5545, 3.1), ending in `eol`.
107pub(crate) fn fold(line: &str, eol: &str) -> String {
108 let mut out = String::with_capacity(line.len() + eol.len() * (line.len() / 74 + 1));
109 let mut width = 0;
110 for c in line.chars() {
111 if width + c.len_utf8() > 75 {
112 out.push_str(eol);
113 out.push(' ');
114 width = 1;
115 }
116 out.push(c);
117 width += c.len_utf8();
118 }
119 out + eol
120}
121
122/// RFC 5545, 3.3.11. A TZID property is TEXT, so `Athens\, Bucharest`
123/// there names the TZID parameter `"Athens, Bucharest"`.
124pub(crate) fn unescape_text(s: &str) -> String {
125 let mut out = String::with_capacity(s.len());
126 let mut chars = s.chars();
127 while let Some(c) = chars.next() {
128 match (c, chars.clone().next()) {
129 ('\\', Some(n @ ('\\' | ';' | ',' | 'n' | 'N'))) => {
130 out.push(if n.eq_ignore_ascii_case(&'n') {
131 '\n'
132 } else {
133 n
134 });
135 chars.next();
136 }
137 _ => out.push(c),
138 }
139 }
140 out
141}
142
143/// RFC 5545, 3.3.11.
144pub(crate) fn escape_text(s: &str) -> String {
145 s.replace('\\', "\\\\")
146 .replace(';', "\\;")
147 .replace(',', "\\,")
148 .replace('\n', "\\n")
149 .replace('\r', "")
150 .replace(|c: char| c.is_control() && c != '\t', "")
151}
152