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