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 raw parameters of a line, split at `;` outside quoted values. A
56/// quoted TZID may hold `;` and `:`.
57pub(crate) fn param_parts(line: &str) -> Vec<&str> {
58 let head = &line[..value_start(line).saturating_sub(1)];
59 let mut parts = Vec::new();
60 let (mut quoted, mut start) = (false, 0);
61 for (i, c) in head.char_indices() {
62 match c {
63 '"' => quoted = !quoted,
64 ';' if !quoted => {
65 parts.push(&head[start..i]);
66 start = i + 1;
67 }
68 _ => {}
69 }
70 }
71 parts.push(&head[start..]);
72 parts.remove(0);
73 parts
74}
75
76/// The value of parameter `key` of an unfolded line, without quotes.
77pub(crate) fn param(line: &str, key: &str) -> Option<String> {
78 param_parts(line).into_iter().find_map(|p| {
79 let (k, v) = p.split_once('=')?;
80 k.trim()
81 .eq_ignore_ascii_case(key)
82 .then(|| v.trim().trim_matches('"').to_string())
83 })
84}
85
86/// An unfolded line folded at 75 octets (RFC 5545, 3.1), ending in `eol`.
87pub(crate) fn fold(line: &str, eol: &str) -> String {
88 let mut out = String::with_capacity(line.len() + eol.len() * (line.len() / 74 + 1));
89 let mut width = 0;
90 for c in line.chars() {
91 if width + c.len_utf8() > 75 {
92 out.push_str(eol);
93 out.push(' ');
94 width = 1;
95 }
96 out.push(c);
97 width += c.len_utf8();
98 }
99 out + eol
100}
101
102/// RFC 5545, 3.3.11. A TZID property is TEXT, so `Athens\, Bucharest`
103/// there names the TZID parameter `"Athens, Bucharest"`.
104pub(crate) fn unescape_text(s: &str) -> String {
105 let mut out = String::with_capacity(s.len());
106 let mut chars = s.chars();
107 while let Some(c) = chars.next() {
108 match (c, chars.clone().next()) {
109 ('\\', Some(n @ ('\\' | ';' | ',' | 'n' | 'N'))) => {
110 out.push(if n.eq_ignore_ascii_case(&'n') {
111 '\n'
112 } else {
113 n
114 });
115 chars.next();
116 }
117 _ => out.push(c),
118 }
119 }
120 out
121}
122
123/// RFC 5545, 3.3.11.
124pub(crate) fn escape_text(s: &str) -> String {
125 s.replace('\\', "\\\\")
126 .replace(';', "\\;")
127 .replace(',', "\\,")
128 .replace('\n', "\\n")
129 .replace('\r', "")
130}
131