]>
Commit | Line | Data |
---|---|---|
9fa01778 | 1 | // FIXME: we have some long lines that could be refactored, but it's not a big deal. |
13cf67c4 XL |
2 | // ignore-tidy-linelength |
3 | ||
9fa01778 | 4 | |
13cf67c4 XL |
5 | |
6 | use std::collections::HashMap; | |
7 | use std::io; | |
8 | use std::io::{Read, Write}; | |
9 | use regex::{Regex, Captures}; | |
10 | ||
11 | fn main() { | |
13cf67c4 XL |
12 | write_md(parse_links(parse_references(read_md()))); |
13 | } | |
14 | ||
15 | fn read_md() -> String { | |
16 | let mut buffer = String::new(); | |
17 | match io::stdin().read_to_string(&mut buffer) { | |
18 | Ok(_) => buffer, | |
19 | Err(error) => panic!(error), | |
20 | } | |
21 | } | |
22 | ||
23 | fn write_md(output: String) { | |
24 | write!(io::stdout(), "{}", output).unwrap(); | |
25 | } | |
26 | ||
27 | fn parse_references(buffer: String) -> (String, HashMap<String, String>) { | |
28 | let mut ref_map = HashMap::new(); | |
9fa01778 | 29 | // FIXME: currently doesn't handle "title" in following line. |
13cf67c4 | 30 | let re = Regex::new(r###"(?m)\n?^ {0,3}\[([^]]+)\]:[[:blank:]]*(.*)$"###).unwrap(); |
9fa01778 | 31 | let output = re.replace_all(&buffer, |caps: &Captures<'_>| { |
13cf67c4 XL |
32 | let key = caps.at(1).unwrap().to_owned().to_uppercase(); |
33 | let val = caps.at(2).unwrap().to_owned(); | |
34 | if ref_map.insert(key, val).is_some() { | |
35 | panic!("Did not expect markdown page to have duplicate reference"); | |
36 | } | |
37 | "".to_string() | |
38 | }); | |
39 | (output, ref_map) | |
40 | } | |
41 | ||
42 | fn parse_links((buffer, ref_map): (String, HashMap<String, String>)) -> String { | |
9fa01778 | 43 | // FIXME: check which punctuation is allowed by spec. |
13cf67c4 XL |
44 | let re = Regex::new(r###"(?:(?P<pre>(?:```(?:[^`]|`[^`])*`?\n```\n)|(?:[^[]`[^`\n]+[\n]?[^`\n]*`))|(?:\[(?P<name>[^]]+)\](?:(?:\([[:blank:]]*(?P<val>[^")]*[^ ])(?:[[:blank:]]*"[^"]*")?\))|(?:\[(?P<key>[^]]*)\]))?))"###).expect("could not create regex"); |
45 | let error_code = Regex::new(r###"^E\d{4}$"###).expect("could not create regex"); | |
9fa01778 | 46 | let output = re.replace_all(&buffer, |caps: &Captures<'_>| { |
13cf67c4 XL |
47 | match caps.name("pre") { |
48 | Some(pre_section) => format!("{}", pre_section.to_owned()), | |
49 | None => { | |
50 | let name = caps.name("name").expect("could not get name").to_owned(); | |
51 | // Really we should ignore text inside code blocks, | |
52 | // this is a hack to not try to treat `#[derive()]`, | |
9fa01778 | 53 | // `[profile]`, `[test]`, or `[E\d\d\d\d]` like a link. |
13cf67c4 XL |
54 | if name.starts_with("derive(") || |
55 | name.starts_with("profile") || | |
56 | name.starts_with("test") || | |
57 | error_code.is_match(&name) { | |
58 | return name | |
59 | } | |
60 | ||
61 | let val = match caps.name("val") { | |
9fa01778 | 62 | // `[name](link)` |
13cf67c4 XL |
63 | Some(value) => value.to_owned(), |
64 | None => { | |
65 | match caps.name("key") { | |
66 | Some(key) => { | |
67 | match key { | |
9fa01778 | 68 | // `[name][]` |
13cf67c4 | 69 | "" => format!("{}", ref_map.get(&name.to_uppercase()).expect(&format!("could not find url for the link text `{}`", name))), |
9fa01778 | 70 | // `[name][reference]` |
13cf67c4 XL |
71 | _ => format!("{}", ref_map.get(&key.to_uppercase()).expect(&format!("could not find url for the link text `{}`", key))), |
72 | } | |
73 | } | |
9fa01778 | 74 | // `[name]` as reference |
13cf67c4 XL |
75 | None => format!("{}", ref_map.get(&name.to_uppercase()).expect(&format!("could not find url for the link text `{}`", name))), |
76 | } | |
77 | } | |
78 | }; | |
79 | format!("{} at *{}*", name, val) | |
80 | } | |
81 | } | |
82 | }); | |
83 | output | |
84 | } | |
85 | ||
86 | #[cfg(test)] | |
87 | mod tests { | |
88 | fn parse(source: String) -> String { | |
89 | super::parse_links(super::parse_references(source)) | |
90 | } | |
91 | ||
92 | #[test] | |
93 | fn parses_inline_link() { | |
94 | let source = r"This is a [link](http://google.com) that should be expanded".to_string(); | |
95 | let target = r"This is a link at *http://google.com* that should be expanded".to_string(); | |
96 | assert_eq!(parse(source), target); | |
97 | } | |
98 | ||
99 | #[test] | |
100 | fn parses_multiline_links() { | |
101 | let source = r"This is a [link](http://google.com) that | |
102 | should appear expanded. Another [location](/here/) and [another](http://gogogo)" | |
103 | .to_string(); | |
104 | let target = r"This is a link at *http://google.com* that | |
105 | should appear expanded. Another location at */here/* and another at *http://gogogo*" | |
106 | .to_string(); | |
107 | assert_eq!(parse(source), target); | |
108 | } | |
109 | ||
110 | #[test] | |
111 | fn parses_reference() { | |
112 | let source = r"This is a [link][theref]. | |
113 | [theref]: http://example.com/foo | |
114 | more text" | |
115 | .to_string(); | |
116 | let target = r"This is a link at *http://example.com/foo*. | |
117 | more text" | |
118 | .to_string(); | |
119 | assert_eq!(parse(source), target); | |
120 | } | |
121 | ||
122 | #[test] | |
123 | fn parses_implicit_link() { | |
124 | let source = r"This is an [implicit][] link. | |
125 | [implicit]: /The Link/" | |
126 | .to_string(); | |
127 | let target = r"This is an implicit at */The Link/* link.".to_string(); | |
128 | assert_eq!(parse(source), target); | |
129 | } | |
130 | #[test] | |
131 | fn parses_refs_with_one_space_indentation() { | |
132 | let source = r"This is a [link][ref] | |
133 | [ref]: The link" | |
134 | .to_string(); | |
135 | let target = r"This is a link at *The link*".to_string(); | |
136 | assert_eq!(parse(source), target); | |
137 | } | |
138 | ||
139 | #[test] | |
140 | fn parses_refs_with_two_space_indentation() { | |
141 | let source = r"This is a [link][ref] | |
142 | [ref]: The link" | |
143 | .to_string(); | |
144 | let target = r"This is a link at *The link*".to_string(); | |
145 | assert_eq!(parse(source), target); | |
146 | } | |
147 | ||
148 | #[test] | |
149 | fn parses_refs_with_three_space_indentation() { | |
150 | let source = r"This is a [link][ref] | |
151 | [ref]: The link" | |
152 | .to_string(); | |
153 | let target = r"This is a link at *The link*".to_string(); | |
154 | assert_eq!(parse(source), target); | |
155 | } | |
156 | ||
157 | #[test] | |
158 | #[should_panic] | |
159 | fn rejects_refs_with_four_space_indentation() { | |
160 | let source = r"This is a [link][ref] | |
161 | [ref]: The link" | |
162 | .to_string(); | |
163 | let target = r"This is a link at *The link*".to_string(); | |
164 | assert_eq!(parse(source), target); | |
165 | } | |
166 | ||
167 | #[test] | |
168 | fn ignores_optional_inline_title() { | |
169 | let source = r###"This is a titled [link](http://example.com "My title")."###.to_string(); | |
170 | let target = r"This is a titled link at *http://example.com*.".to_string(); | |
171 | assert_eq!(parse(source), target); | |
172 | } | |
173 | ||
174 | #[test] | |
175 | fn parses_title_with_puctuation() { | |
176 | let source = r###"[link](http://example.com "It's Title")"###.to_string(); | |
177 | let target = r"link at *http://example.com*".to_string(); | |
178 | assert_eq!(parse(source), target); | |
179 | } | |
180 | ||
181 | #[test] | |
182 | fn parses_name_with_punctuation() { | |
183 | let source = r###"[I'm here](there)"###.to_string(); | |
184 | let target = r###"I'm here at *there*"###.to_string(); | |
185 | assert_eq!(parse(source), target); | |
186 | } | |
187 | #[test] | |
188 | fn parses_name_with_utf8() { | |
189 | let source = r###"[user’s forum](the user’s forum)"###.to_string(); | |
190 | let target = r###"user’s forum at *the user’s forum*"###.to_string(); | |
191 | assert_eq!(parse(source), target); | |
192 | } | |
193 | ||
194 | ||
195 | #[test] | |
196 | fn parses_reference_with_punctuation() { | |
197 | let source = r###"[link][the ref-ref] | |
198 | [the ref-ref]:http://example.com/ref-ref"### | |
199 | .to_string(); | |
200 | let target = r###"link at *http://example.com/ref-ref*"###.to_string(); | |
201 | assert_eq!(parse(source), target); | |
202 | } | |
203 | ||
204 | #[test] | |
205 | fn parses_reference_case_insensitively() { | |
206 | let source = r"[link][Ref] | |
207 | [ref]: The reference" | |
208 | .to_string(); | |
209 | let target = r"link at *The reference*".to_string(); | |
210 | assert_eq!(parse(source), target); | |
211 | } | |
212 | #[test] | |
213 | fn parses_link_as_reference_when_reference_is_empty() { | |
214 | let source = r"[link as reference][] | |
215 | [link as reference]: the actual reference" | |
216 | .to_string(); | |
217 | let target = r"link as reference at *the actual reference*".to_string(); | |
218 | assert_eq!(parse(source), target); | |
219 | } | |
220 | ||
221 | #[test] | |
222 | fn parses_link_without_reference_as_reference() { | |
223 | let source = r"[link] is alone | |
224 | [link]: The contents" | |
225 | .to_string(); | |
226 | let target = r"link at *The contents* is alone".to_string(); | |
227 | assert_eq!(parse(source), target); | |
228 | } | |
229 | ||
230 | #[test] | |
231 | #[ignore] | |
232 | fn parses_link_without_reference_as_reference_with_asterisks() { | |
233 | let source = r"*[link]* is alone | |
234 | [link]: The contents" | |
235 | .to_string(); | |
236 | let target = r"*link* at *The contents* is alone".to_string(); | |
237 | assert_eq!(parse(source), target); | |
238 | } | |
239 | #[test] | |
240 | fn ignores_links_in_pre_sections() { | |
241 | let source = r###"```toml | |
242 | [package] | |
243 | name = "hello_cargo" | |
244 | version = "0.1.0" | |
245 | authors = ["Your Name <you@example.com>"] | |
246 | ||
247 | [dependencies] | |
248 | ``` | |
249 | "### | |
250 | .to_string(); | |
251 | let target = source.clone(); | |
252 | assert_eq!(parse(source), target); | |
253 | } | |
254 | ||
255 | #[test] | |
256 | fn ignores_links_in_quoted_sections() { | |
257 | let source = r###"do not change `[package]`."###.to_string(); | |
258 | let target = source.clone(); | |
259 | assert_eq!(parse(source), target); | |
260 | } | |
261 | #[test] | |
262 | fn ignores_links_in_quoted_sections_containing_newlines() { | |
263 | let source = r"do not change `this [package] | |
264 | is still here` [link](ref)" | |
265 | .to_string(); | |
266 | let target = r"do not change `this [package] | |
267 | is still here` link at *ref*" | |
268 | .to_string(); | |
269 | assert_eq!(parse(source), target); | |
270 | } | |
271 | ||
272 | #[test] | |
273 | fn ignores_links_in_pre_sections_while_still_handling_links() { | |
274 | let source = r###"```toml | |
275 | [package] | |
276 | name = "hello_cargo" | |
277 | version = "0.1.0" | |
278 | authors = ["Your Name <you@example.com>"] | |
279 | ||
280 | [dependencies] | |
281 | ``` | |
282 | Another [link] | |
283 | more text | |
284 | [link]: http://gohere | |
285 | "### | |
286 | .to_string(); | |
287 | let target = r###"```toml | |
288 | [package] | |
289 | name = "hello_cargo" | |
290 | version = "0.1.0" | |
291 | authors = ["Your Name <you@example.com>"] | |
292 | ||
293 | [dependencies] | |
294 | ``` | |
295 | Another link at *http://gohere* | |
296 | more text | |
297 | "### | |
298 | .to_string(); | |
299 | assert_eq!(parse(source), target); | |
300 | } | |
301 | #[test] | |
302 | fn ignores_quotes_in_pre_sections() { | |
303 | let source = r###"```bash | |
304 | $ cargo build | |
305 | Compiling guessing_game v0.1.0 (file:///projects/guessing_game) | |
306 | src/main.rs:23:21: 23:35 error: mismatched types [E0308] | |
307 | src/main.rs:23 match guess.cmp(&secret_number) { | |
308 | ^~~~~~~~~~~~~~ | |
309 | src/main.rs:23:21: 23:35 help: run `rustc --explain E0308` to see a detailed explanation | |
310 | src/main.rs:23:21: 23:35 note: expected type `&std::string::String` | |
311 | src/main.rs:23:21: 23:35 note: found type `&_` | |
312 | error: aborting due to previous error | |
313 | Could not compile `guessing_game`. | |
314 | ``` | |
315 | "### | |
316 | .to_string(); | |
317 | let target = source.clone(); | |
318 | assert_eq!(parse(source), target); | |
319 | } | |
320 | #[test] | |
321 | fn ignores_short_quotes() { | |
322 | let source = r"to `1` at index `[0]` i".to_string(); | |
323 | let target = source.clone(); | |
324 | assert_eq!(parse(source), target); | |
325 | } | |
326 | #[test] | |
327 | fn ignores_pre_sections_with_final_quote() { | |
328 | let source = r###"```bash | |
329 | $ cargo run | |
330 | Compiling points v0.1.0 (file:///projects/points) | |
331 | error: the trait bound `Point: std::fmt::Display` is not satisfied [--explain E0277] | |
332 | --> src/main.rs:8:29 | |
333 | 8 |> println!("Point 1: {}", p1); | |
334 | |> ^^ | |
335 | <std macros>:2:27: 2:58: note: in this expansion of format_args! | |
336 | <std macros>:3:1: 3:54: note: in this expansion of print! (defined in <std macros>) | |
337 | src/main.rs:8:5: 8:33: note: in this expansion of println! (defined in <std macros>) | |
338 | note: `Point` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string | |
339 | note: required by `std::fmt::Display::fmt` | |
340 | ``` | |
341 | `here` is another [link](the ref) | |
342 | "###.to_string(); | |
343 | let target = r###"```bash | |
344 | $ cargo run | |
345 | Compiling points v0.1.0 (file:///projects/points) | |
346 | error: the trait bound `Point: std::fmt::Display` is not satisfied [--explain E0277] | |
347 | --> src/main.rs:8:29 | |
348 | 8 |> println!("Point 1: {}", p1); | |
349 | |> ^^ | |
350 | <std macros>:2:27: 2:58: note: in this expansion of format_args! | |
351 | <std macros>:3:1: 3:54: note: in this expansion of print! (defined in <std macros>) | |
352 | src/main.rs:8:5: 8:33: note: in this expansion of println! (defined in <std macros>) | |
353 | note: `Point` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string | |
354 | note: required by `std::fmt::Display::fmt` | |
355 | ``` | |
356 | `here` is another link at *the ref* | |
357 | "###.to_string(); | |
358 | assert_eq!(parse(source), target); | |
359 | } | |
360 | #[test] | |
361 | fn parses_adam_p_cheatsheet() { | |
362 | let source = r###"[I'm an inline-style link](https://www.google.com) | |
363 | ||
364 | [I'm an inline-style link with title](https://www.google.com "Google's Homepage") | |
365 | ||
366 | [I'm a reference-style link][Arbitrary case-insensitive reference text] | |
367 | ||
368 | [I'm a relative reference to a repository file](../blob/master/LICENSE) | |
369 | ||
370 | [You can use numbers for reference-style link definitions][1] | |
371 | ||
372 | Or leave it empty and use the [link text itself][]. | |
373 | ||
374 | URLs and URLs in angle brackets will automatically get turned into links. | |
375 | http://www.example.com or <http://www.example.com> and sometimes | |
376 | example.com (but not on Github, for example). | |
377 | ||
378 | Some text to show that the reference links can follow later. | |
379 | ||
380 | [arbitrary case-insensitive reference text]: https://www.mozilla.org | |
381 | [1]: http://slashdot.org | |
382 | [link text itself]: http://www.reddit.com"### | |
383 | .to_string(); | |
384 | ||
385 | let target = r###"I'm an inline-style link at *https://www.google.com* | |
386 | ||
387 | I'm an inline-style link with title at *https://www.google.com* | |
388 | ||
389 | I'm a reference-style link at *https://www.mozilla.org* | |
390 | ||
391 | I'm a relative reference to a repository file at *../blob/master/LICENSE* | |
392 | ||
393 | You can use numbers for reference-style link definitions at *http://slashdot.org* | |
394 | ||
395 | Or leave it empty and use the link text itself at *http://www.reddit.com*. | |
396 | ||
397 | URLs and URLs in angle brackets will automatically get turned into links. | |
398 | http://www.example.com or <http://www.example.com> and sometimes | |
399 | example.com (but not on Github, for example). | |
400 | ||
401 | Some text to show that the reference links can follow later. | |
402 | "### | |
403 | .to_string(); | |
404 | assert_eq!(parse(source), target); | |
405 | } | |
13cf67c4 | 406 | } |