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