|
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 } |
|