1use std::fmt;
10
11use crate::ast::{DateBound, DateValue, FilterKind, QueryField, QueryNode, RelativeUnit, SizeOp};
12
13impl fmt::Display for QueryNode {
14 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15 match self {
16 Self::Text(s) => write_term(f, s),
17 Self::Exact(s) => {
18 f.write_str("+")?;
19 f.write_str(s)
20 }
21 Self::Phrase(s) => write_quoted(f, s),
22 Self::Field { field, value } => {
23 write!(f, "{}:", field_name(*field))?;
24 write_value(f, value)
25 }
26 Self::Filter(kind) => write_filter(f, kind),
27 Self::Label(name) => {
28 f.write_str("label:")?;
29 write_value(f, name)
30 }
31 Self::DateRange { bound, date } => write_date(f, *bound, date),
32 Self::Size { op, bytes } => write_size(f, *op, *bytes),
33 Self::Near {
34 left,
35 right,
36 distance,
37 } => write!(f, "\"{left} AROUND {distance} {right}\""),
38 Self::And(left, right) => {
39 write_compound(f, left)?;
40 f.write_str(" AND ")?;
41 write_compound(f, right)
42 }
43 Self::Or(left, right) => {
44 write_compound(f, left)?;
45 f.write_str(" OR ")?;
46 write_compound(f, right)
47 }
48 Self::Not(inner) => {
49 f.write_str("-")?;
50 write_compound(f, inner)
51 }
52 }
53 }
54}
55
56fn write_compound(f: &mut fmt::Formatter<'_>, node: &QueryNode) -> fmt::Result {
59 match node {
60 QueryNode::And(..) | QueryNode::Or(..) | QueryNode::Not(..) => {
61 write!(f, "({node})")
62 }
63 other => write!(f, "{other}"),
64 }
65}
66
67fn write_term(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
68 if needs_quoting(s) {
69 write_quoted(f, s)
70 } else {
71 f.write_str(s)
72 }
73}
74
75fn write_value(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
76 if needs_quoting(s) {
77 write_quoted(f, s)
78 } else {
79 f.write_str(s)
80 }
81}
82
83fn needs_quoting(s: &str) -> bool {
84 s.is_empty()
85 || s.chars()
86 .any(|c| c.is_whitespace() || matches!(c, '"' | '(' | ')' | '{' | '}' | ':'))
87}
88
89fn write_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
90 f.write_str("\"")?;
91 for c in s.chars() {
92 if c == '"' {
93 f.write_str("\\\"")?;
94 } else {
95 f.write_str(&c.to_string())?;
96 }
97 }
98 f.write_str("\"")
99}
100
101fn field_name(field: QueryField) -> &'static str {
102 match field {
103 QueryField::From => "from",
104 QueryField::To => "to",
105 QueryField::Cc => "cc",
106 QueryField::Bcc => "bcc",
107 QueryField::Subject => "subject",
108 QueryField::Body => "body",
109 QueryField::Filename => "filename",
110 QueryField::List => "list",
111 QueryField::DeliveredTo => "deliveredto",
112 QueryField::Rfc822MsgId => "rfc822msgid",
113 }
114}
115
116fn write_filter(f: &mut fmt::Formatter<'_>, kind: &FilterKind) -> fmt::Result {
117 match kind {
118 FilterKind::Unread => f.write_str("is:unread"),
119 FilterKind::Read => f.write_str("is:read"),
120 FilterKind::Starred => f.write_str("is:starred"),
121 FilterKind::Draft => f.write_str("is:draft"),
122 FilterKind::Sent => f.write_str("is:sent"),
123 FilterKind::Trash => f.write_str("is:trash"),
124 FilterKind::Spam => f.write_str("is:spam"),
125 FilterKind::Answered => f.write_str("is:answered"),
126 FilterKind::Inbox => f.write_str("is:inbox"),
127 FilterKind::Archived => f.write_str("is:archived"),
128 FilterKind::Anywhere => f.write_str("in:anywhere"),
129 FilterKind::HasAttachment => f.write_str("has:attachment"),
130 FilterKind::HasCalendar => f.write_str("has:calendar"),
131 FilterKind::HasUserLabels => f.write_str("has:userlabels"),
132 FilterKind::NoUserLabels => f.write_str("has:nouserlabels"),
133 FilterKind::HasDrive => f.write_str("has:drive"),
134 FilterKind::HasDocument => f.write_str("has:document"),
135 FilterKind::HasSpreadsheet => f.write_str("has:spreadsheet"),
136 FilterKind::HasPresentation => f.write_str("has:presentation"),
137 FilterKind::HasYoutube => f.write_str("has:youtube"),
138 FilterKind::HasInlineImage => f.write_str("has:inline"),
139 FilterKind::HasLink => f.write_str("has:link"),
140 FilterKind::HasLinkHeavy => f.write_str("has:link-heavy"),
141 FilterKind::NoLinks => f.write_str("has:link-none"),
142 FilterKind::Custom(name) => write!(f, "is:{name}"),
143 }
144}
145
146fn write_date(f: &mut fmt::Formatter<'_>, bound: DateBound, date: &DateValue) -> fmt::Result {
147 match (bound, date) {
150 (DateBound::Before, DateValue::Relative { amount, unit }) => {
151 write!(f, "older_than:{amount}{}", unit_suffix(*unit))
152 }
153 (DateBound::After, DateValue::Relative { amount, unit }) => {
154 write!(f, "newer_than:{amount}{}", unit_suffix(*unit))
155 }
156 _ => {
157 let prefix = match bound {
158 DateBound::After => "after",
159 DateBound::Before => "before",
160 DateBound::Exact => "date",
161 };
162 f.write_str(prefix)?;
163 f.write_str(":")?;
164 write_date_value(f, date)
165 }
166 }
167}
168
169fn write_date_value(f: &mut fmt::Formatter<'_>, date: &DateValue) -> fmt::Result {
170 match date {
171 DateValue::Specific(d) => write!(f, "{}", d.format("%Y-%m-%d")),
172 DateValue::Today => f.write_str("today"),
173 DateValue::Yesterday => f.write_str("yesterday"),
174 DateValue::ThisWeek => f.write_str("this-week"),
175 DateValue::ThisMonth => f.write_str("this-month"),
176 DateValue::Relative { amount, unit } => {
178 write!(f, "{amount}{}", unit_suffix(*unit))
179 }
180 }
181}
182
183fn unit_suffix(unit: RelativeUnit) -> &'static str {
184 match unit {
185 RelativeUnit::Day => "d",
186 RelativeUnit::Week => "w",
187 RelativeUnit::Month => "m",
188 RelativeUnit::Year => "y",
189 }
190}
191
192fn write_size(f: &mut fmt::Formatter<'_>, op: SizeOp, bytes: u64) -> fmt::Result {
193 let op_str = match op {
194 SizeOp::LessThan => "<",
195 SizeOp::LessThanOrEqual => "<=",
196 SizeOp::Equal => "=",
197 SizeOp::GreaterThan => ">",
198 SizeOp::GreaterThanOrEqual => ">=",
199 };
200 write!(f, "size:{op_str}{bytes}")
202}