18e381ea Andy McCall 1 month ago ↶
1
2
3
4
5
6
pub struct Numbers < 'a > {
7
pub repo_url : & 'a str ,
8
pub issues : & 'a [i64 ],
9
pub proposals : & 'a [i64 ],
10
}
11
12
impl Numbers < '_ > {
13
fn url_for ( & self , n : i64 ) -> Option< String> {
14
if self .proposals.contains ( & n) {
15
Some ( format! ( " {} /proposal/{n} " , self .repo_url) )
16
} else if self .issues.contains ( & n) {
17
Some ( format! ( " {} /issue/{n} " , self .repo_url) )
18
} else {
19
None
20
}
21
}
22
}
23
24
25
26
pub fn link ( text : & str , numbers : & Numbers< '_ > ) -> String {
27
let mut out = String :: with_capacity( text.len ( ) + 64 ) ;
28
let mut in_fence = false ;
29
for line in text.split_inclusive ( ' \n ' ) {
30
if line.trim_start ( ) .starts_with ( " ```" ) {
31
in_fence = ! in_fence;
32
out.push_str ( line) ;
33
continue ;
34
}
35
if in_fence {
36
out.push_str ( line) ;
37
continue ;
38
}
39
40
for ( i, seg) in line.split ( ' `' ) .enumerate ( ) {
41
if i > 0 {
42
out.push ( ' `' ) ;
43
}
44
if i % 2 = = 0 {
45
out.push_str ( & link_prose ( seg, numbers) ) ;
46
} else {
47
out.push_str ( seg) ;
48
}
49
}
50
}
51
out
52
}
53
54
fn link_prose ( seg : & str , numbers : & Numbers< '_ > ) -> String {
55
let re = regex:: Regex:: new( r "(?P<pre>^|[^\w/\[])(?:(?P<slug>[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*)#(?P<xn>\d+)|#(?P<n>\d+)|@(?P<user>[A-Za-z0-9][A-Za-z0-9._-]{0,63})|(?P<hash>\b[0-9a-f]{12,64}\b))" ) .unwrap ( ) ;
56
re.replace_all ( seg, | c : & regex:: Captures| {
57
let pre = & c[ " pre" ] ;
58
if let Some ( slug) = c.name ( " slug" ) {
59
let n = & c[ " xn" ] ;
60
return format! ( " {pre} [{} #{n} ](/{} /issue/{n} )" , slug.as_str ( ) , slug.as_str ( ) ) ;
61
}
62
if let Some ( n) = c.name ( " n" ) {
63
let num: i64 = n.as_str ( ) .parse ( ) .unwrap_or ( 0 ) ;
64
return match numbers.url_for ( num) {
65
Some ( url) => format! ( " {pre} [#{num} ]({url} )" ) ,
66
None => c[ 0 ] .to_string ( ) ,
67
} ;
68
}
69
if let Some ( u) = c.name ( " user" ) {
70
let name = u.as_str ( ) .trim_end_matches ( ' .' ) ;
71
return format! ( " {pre} [@{name} ](/{name} )" ) ;
72
}
73
if let Some ( h) = c.name ( " hash" ) {
74
let hex = h.as_str ( ) ;
75
return format! ( " {pre} [{} ]({} /change/{hex} )" , & hex[ .. 12. min ( hex.len ( ) ) ] , numbers.repo_url) ;
76
}
77
c[ 0 ] .to_string ( )
78
} )
79
.into_owned ( )
80
}
81
82
83
84
pub fn closes ( message : & str ) -> Vec< i64 > {
85
let re = regex:: Regex:: new( r "(?i)\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)" ) .unwrap ( ) ;
86
let mut out: Vec< i64 > = re.captures_iter ( message) .filter_map ( | c | c[ 1 ] .parse ( ) .ok ( ) ) .collect ( ) ;
87
out.sort ( ) ;
88
out.dedup ( ) ;
89
out
90
}
91
92
# [ cfg ( test ) ]
93
mod tests {
94
use super :: * ;
95
96
# [ test ]
97
fn links_references_outside_code ( ) {
98
let n = Numbers { repo_url: " /a/b" , issues: & [ 3 ] , proposals: & [ 7 ] } ;
99
let out = link ( " see #3 and #7 and #9, `#3` too, by @andy; a/b#3 and other/repo#5; hash deadbeefcafe1234\n ```\n #3 @x\n ```\n " , & n) ;
100
assert! ( out.contains ( " [#3](/a/b/issue/3)" ) , " {out}" ) ;
101
assert! ( out.contains ( " [#7](/a/b/proposal/7)" ) , " {out}" ) ;
102
assert! ( out.contains ( " #9," ) , " unknown numbers stay: {out}" ) ;
103
assert! ( out.contains ( " `#3`" ) , " code spans untouched: {out}" ) ;
104
assert! ( out.contains ( " [@andy](/andy)" ) , " {out}" ) ;
105
assert! ( out.contains ( " [other/repo#5](/other/repo/issue/5)" ) , " {out}" ) ;
106
assert! ( out.contains ( " [deadbeefcafe](/a/b/change/deadbeefcafe1234)" ) , " {out}" ) ;
107
assert! ( out.contains ( " ```\n #3 @x\n ```" ) , " fences untouched: {out}" ) ;
108
}
109
110
# [ test ]
111
fn closing_keywords ( ) {
112
assert_eq! ( closes ( " Fixes #12 and closes #3, resolved #12" ) , vec! [ 3 , 12 ] ) ;
113
assert! ( closes ( " mentions #4 only" ) .is_empty ( ) ) ;
114
}
115
}