Skip to content

Commit 2d3651d

Browse files
committed
Merge remote-tracking branch 'origin/pr/197'
2 parents 9b70ed3 + f44330d commit 2d3651d

File tree

1 file changed

+396
-0
lines changed

1 file changed

+396
-0
lines changed

src/bin/link2print.rs

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

0 commit comments

Comments
 (0)