| |
1 // The main documentation is in the README. |
| |
2 #![doc = include_str!("../README.md")] |
| |
3 |
| 1 use std::io; |
4 use std::io; |
| 2 use std::io::BufWriter; |
5 use std::io::BufWriter; |
| 3 use std::io::Write; |
6 use std::io::Write; |
| |
7 use clap::Parser; |
| |
8 |
| |
9 /// Command line parameters |
| |
10 #[derive(Parser, Debug)] |
| |
11 #[clap( |
| |
12 about = env!("CARGO_PKG_DESCRIPTION"), |
| |
13 author = env!("CARGO_PKG_AUTHORS"), |
| |
14 version = env!("CARGO_PKG_VERSION"), |
| |
15 )] |
| |
16 struct CommandLineArgs { |
| |
17 #[arg(long, short = 'c')] |
| |
18 /// Strip comments |
| |
19 strip_comments : bool, |
| |
20 |
| |
21 #[arg(long, short = 'w')] |
| |
22 /// Strip unnecessary whitespace |
| |
23 strip_whitespace : bool, |
| |
24 } |
| 4 |
25 |
| 5 #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
26 #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 6 enum Element { |
27 enum Element { |
| 7 Added, |
28 Added, |
| 8 Deleted, |
29 Deleted, |
| 16 Output(Element), |
37 Output(Element), |
| 17 Ignore(Element), |
38 Ignore(Element), |
| 18 Scan(Element, bool), |
39 Scan(Element, bool), |
| 19 } |
40 } |
| 20 |
41 |
| |
42 use Status::*; |
| |
43 use Element::*; |
| |
44 |
| |
45 struct Out<W : Write> { |
| |
46 only_whitespace : bool, |
| |
47 stored_whitespace : String, |
| |
48 output : W, |
| |
49 stack : Vec<Status>, |
| |
50 whitespace_satisfied : bool, |
| |
51 par_satisfied : bool, |
| |
52 } |
| |
53 |
| |
54 impl<W : Write> Out<W> { |
| |
55 fn current(&self) -> Status { |
| |
56 self.stack.last().map_or(Output(Other), |s| *s) |
| |
57 } |
| |
58 |
| |
59 fn raw_out(&mut self, c : char) { |
| |
60 write!(self.output, "{}", c).unwrap(); |
| |
61 } |
| |
62 |
| |
63 pub fn out(&mut self, c : char) { |
| |
64 self.only_whitespace = false; |
| |
65 write!(self.output, "{}{}", self.stored_whitespace, c).unwrap(); |
| |
66 self.stored_whitespace.clear(); |
| |
67 self.whitespace_satisfied = false; |
| |
68 self.par_satisfied = false; |
| |
69 } |
| |
70 |
| |
71 pub fn whitespace(&mut self, c : char) { |
| |
72 self.stored_whitespace.push(c); |
| |
73 } |
| |
74 |
| |
75 pub fn line_end(&mut self, strip_ws : bool, input_only_ws : bool) { |
| |
76 let cur = self.current(); |
| |
77 let skip_linefeed = if input_only_ws { |
| |
78 // Need a paragraph break |
| |
79 strip_ws && self.par_satisfied |
| |
80 } else if strip_ws { |
| |
81 self.only_whitespace && self.whitespace_satisfied |
| |
82 } else if let Ignore(Comment) = cur { |
| |
83 // Skip comment-only lines if the comment is ignored |
| |
84 self.only_whitespace |
| |
85 } else if let Ignore(_) = cur { |
| |
86 // Skip line feeds in ignored bits |
| |
87 true |
| |
88 } else { |
| |
89 false |
| |
90 }; |
| |
91 |
| |
92 if !skip_linefeed { |
| |
93 if !strip_ws { |
| |
94 write!(self.output, "{}", self.stored_whitespace).unwrap(); |
| |
95 } |
| |
96 self.raw_out('\n'); |
| |
97 self.whitespace_satisfied = true; |
| |
98 self.par_satisfied = self.only_whitespace; |
| |
99 } |
| |
100 |
| |
101 if let Ignore(Comment) | Output(Comment) = cur { |
| |
102 self.stack.pop(); |
| |
103 } |
| |
104 |
| |
105 self.stored_whitespace.clear(); |
| |
106 self.only_whitespace = true; |
| |
107 } |
| |
108 |
| |
109 pub fn flush(&mut self) { |
| |
110 self.output.flush().unwrap(); |
| |
111 } |
| |
112 } |
| |
113 |
| 21 fn main() { |
114 fn main() { |
| |
115 let cli = CommandLineArgs::parse(); |
| 22 let input = io::stdin(); |
116 let input = io::stdin(); |
| 23 let mut output = BufWriter::new(io::stdout()); |
117 |
| 24 let mut status_stack = Vec::new(); |
118 let mut o = Out { |
| 25 |
119 only_whitespace : true, |
| 26 use Status::*; |
120 stored_whitespace : String::new(), |
| 27 use Element::*; |
121 output : BufWriter::new(io::stdout()), |
| 28 |
122 stack : Vec::new(), |
| 29 let current = |s : &Vec<Status>| s.last().map_or(Output(Other), |s| *s); |
123 whitespace_satisfied : true, |
| 30 let mut out = |c : char| { write!(output, "{}", c).unwrap(); }; |
124 par_satisfied : true, |
| |
125 }; |
| |
126 |
| 31 let mut lineno = 0; |
127 let mut lineno = 0; |
| 32 |
128 |
| 33 for l in input.lines().map(|l| l.unwrap()) { |
129 for l in input.lines().map(|l| l.unwrap()) { |
| 34 lineno += 1; |
130 lineno += 1; |
| 35 let mut chars = l.chars(); |
131 let mut chars = l.chars(); |
| 36 let started_ignore = if let Ignore(_) = current(&status_stack) { true } else { false }; |
|
| 37 let mut maybe_next_char = None; |
132 let mut maybe_next_char = None; |
| |
133 let mut input_only_ws = true; |
| |
134 |
| 38 'process_line: loop { |
135 'process_line: loop { |
| 39 let next_char = match maybe_next_char { |
136 let next_char = match maybe_next_char { |
| 40 None => chars.next(), |
137 None => chars.next(), |
| 41 Some(c) => { |
138 Some(c) => { |
| 42 maybe_next_char = None; |
139 maybe_next_char = None; |
| 43 Some(c) |
140 Some(c) |
| 44 } |
141 } |
| 45 }; |
142 }; |
| 46 match(current(&status_stack), next_char) { |
143 input_only_ws = input_only_ws && next_char.map_or(true, |c| c.is_whitespace()); |
| |
144 match(o.current(), next_char) { |
| 47 (_, None) => { |
145 (_, None) => { |
| 48 break 'process_line; |
146 break 'process_line; |
| 49 }, |
147 }, |
| 50 (st @ (Output(e) | Ignore(e)), Some('\\')) if e != Comment => { |
148 (st @ (Output(e) | Ignore(e)), Some('\\')) if e != Comment => { |
| 51 let mut command = String::new(); |
149 let mut command = String::new(); |
| 66 first = false; |
164 first = false; |
| 67 }; |
165 }; |
| 68 let output_guard = if let Ignore(_) = st { false } else { true }; |
166 let output_guard = if let Ignore(_) = st { false } else { true }; |
| 69 match command.as_str() { |
167 match command.as_str() { |
| 70 "added" => { |
168 "added" => { |
| 71 status_stack.push(Scan(Added, true && output_guard)); |
169 o.stack.push(Scan(Added, true && output_guard)); |
| 72 }, |
170 }, |
| 73 "replaced" => { |
171 "replaced" => { |
| 74 status_stack.push(Scan(Replaced, true && output_guard)); |
172 o.stack.push(Scan(Replaced, true && output_guard)); |
| 75 }, |
173 }, |
| 76 "deleted" => { |
174 "deleted" => { |
| 77 status_stack.push(Scan(Deleted, false)); |
175 o.stack.push(Scan(Deleted, false)); |
| 78 }, |
176 }, |
| 79 _ => { |
177 _ => { |
| 80 if output_guard { |
178 if output_guard { |
| 81 out('\\'); |
179 o.out('\\'); |
| 82 command.chars().for_each(|c| out(c.clone())); |
180 command.chars().for_each(|c| o.out(c.clone())); |
| 83 } |
181 } |
| 84 } |
182 } |
| 85 }; |
183 }; |
| 86 }, |
184 }, |
| 87 (Scan(next, o), Some(c)) => { |
185 (Scan(next, out), Some(c)) => { |
| 88 match c { |
186 match c { |
| 89 '{' => { |
187 '{' => { |
| 90 status_stack.pop(); |
188 o.stack.pop(); |
| 91 status_stack.push(if o { Output(next) } else { Ignore(next) }); |
189 o.stack.push(if out { Output(next) } else { Ignore(next) }); |
| 92 }, |
190 }, |
| 93 ' ' => { |
191 ' ' => { |
| 94 }, |
192 }, |
| 95 _ => panic!("Non-whitespace character ({c}) separating arguments on\ |
193 _ => panic!("Non-whitespace character ({c}) separating arguments on\ |
| 96 line {lineno}"), |
194 line {lineno}"), |
| 97 } |
195 } |
| 98 }, |
196 }, |
| 99 (Output(e), Some('{')) if e != Comment => { |
197 (Output(e), Some('{')) if e != Comment => { |
| 100 out('{'); |
198 o.out('{'); |
| 101 status_stack.push(Output(Other)); |
199 o.stack.push(Output(Other)); |
| 102 }, |
200 }, |
| 103 (Ignore(e), Some('{')) if e != Comment => { |
201 (Ignore(e), Some('{')) if e != Comment => { |
| 104 status_stack.push(Ignore(Other)); |
202 o.stack.push(Ignore(Other)); |
| 105 }, |
203 }, |
| 106 (Output(Added) | Ignore(Added) | Output(Deleted) | Ignore(Deleted), Some('}')) => { |
204 (Output(Added) | Ignore(Added) | Output(Deleted) | Ignore(Deleted), Some('}')) => { |
| 107 status_stack.pop(); |
205 o.stack.pop(); |
| 108 }, |
206 }, |
| 109 (Output(Replaced) | Ignore(Replaced), Some('}')) => { |
207 (Output(Replaced) | Ignore(Replaced), Some('}')) => { |
| 110 status_stack.pop(); |
208 o.stack.pop(); |
| 111 status_stack.push(Scan(Deleted, false)); |
209 o.stack.push(Scan(Deleted, false)); |
| 112 }, |
210 }, |
| 113 (Output(Other), Some('}')) => { |
211 (Output(Other), Some('}')) => { |
| 114 out('}'); |
212 o.out('}'); |
| 115 status_stack.pop(); |
213 o.stack.pop(); |
| 116 }, |
214 }, |
| 117 (Ignore(_), Some('}')) => { |
215 (Ignore(e), Some('}')) if e != Comment => { |
| 118 status_stack.pop(); |
216 o.stack.pop(); |
| 119 }, |
217 }, |
| 120 (Output(e), Some('%')) if e != Comment=> { |
218 (Output(e), Some('%')) if e != Comment=> { |
| 121 out('%'); |
219 if cli.strip_comments { |
| 122 status_stack.push(Output(Comment)); |
220 if o.stored_whitespace.is_empty() && !o.only_whitespace { |
| |
221 // Output comment marker if it is required to maintain |
| |
222 // lack of whitespace. |
| |
223 o.out('%'); |
| |
224 } |
| |
225 o.stack.push(Ignore(Comment)); |
| |
226 } else { |
| |
227 o.out('%'); |
| |
228 o.stack.push(Output(Comment)); |
| |
229 } |
| 123 }, |
230 }, |
| 124 (Ignore(e), Some('%')) if e != Comment => { |
231 (Ignore(e), Some('%')) if e != Comment => { |
| 125 status_stack.push(Ignore(Comment)); |
232 o.stack.push(Ignore(Comment)); |
| |
233 }, |
| |
234 (Output(_), Some(c)) if c.is_whitespace() => { |
| |
235 o.whitespace(c); |
| 126 }, |
236 }, |
| 127 (Output(_), Some(c)) => { |
237 (Output(_), Some(c)) => { |
| 128 out(c); |
238 o.out(c); |
| 129 }, |
239 }, |
| 130 (Ignore(_), Some(_)) => { |
240 (Ignore(_), Some(_)) => { |
| 131 }, |
241 }, |
| 132 }; |
242 }; |
| 133 } |
243 } |
| 134 match current(&status_stack) { |
244 |
| 135 Ignore(e) => { |
245 o.line_end(cli.strip_whitespace, input_only_ws); |
| 136 if !started_ignore { |
246 } |
| 137 out('\n'); |
247 |
| 138 } |
248 o.flush(); |
| 139 if e == Comment { |
249 } |
| 140 status_stack.pop(); |
|
| 141 } |
|
| 142 }, |
|
| 143 Output(e) => { |
|
| 144 out('\n'); |
|
| 145 if e == Comment { |
|
| 146 status_stack.pop(); |
|
| 147 } |
|
| 148 }, |
|
| 149 Scan(_, _) => { |
|
| 150 }, |
|
| 151 } |
|
| 152 } |
|
| 153 |
|
| 154 output.flush().unwrap(); |
|
| 155 } |
|