|
| 1 | +use regex::{Captures, Regex}; |
| 2 | + |
| 3 | +/// Replaces valid GitHub @mentions with backticks to prevent accidental pings |
| 4 | +/// |
| 5 | +/// For example: |
| 6 | +/// "@user" -> "`@user`". |
| 7 | +/// "@org/team" -> "`@org/team`". |
| 8 | +/// "@org/team/subteam" -> "`@org/team/subteam`". |
| 9 | +pub fn suppress_github_mentions(text: &str) -> String { |
| 10 | + if text.is_empty() || !text.contains('@') { |
| 11 | + return text.to_string(); |
| 12 | + } |
| 13 | + |
| 14 | + let segment = r"[a-zA-Z0-9][a-zA-Z0-9\-]{0,38}"; |
| 15 | + let pattern = format!(r"@{0}(?:/{0})*", segment); |
| 16 | + |
| 17 | + let re = Regex::new(&pattern).unwrap(); |
| 18 | + re.replace_all(text, |caps: &Captures| { |
| 19 | + let entry = caps.get(0).unwrap(); |
| 20 | + let mention = entry.as_str(); |
| 21 | + let start = entry.start(); |
| 22 | + let end = entry.end(); |
| 23 | + |
| 24 | + if !is_valid_mention_context(text, start, end) { |
| 25 | + return mention.to_string(); |
| 26 | + } |
| 27 | + |
| 28 | + format!("`{mention}`") |
| 29 | + }) |
| 30 | + .to_string() |
| 31 | +} |
| 32 | + |
| 33 | +/// Validates mention boundaries according to GitHub's autolinking rules |
| 34 | +/// |
| 35 | +/// A mention is considered valid if: |
| 36 | +/// 1. Preceded by non-word character (or start of string) |
| 37 | +/// 2. Followed by non-word character (or end of string) |
| 38 | +/// |
| 39 | +/// ref: https://github.com/rust-lang/homu/pull/230 |
| 40 | +fn is_valid_mention_context(text: &str, start: usize, end: usize) -> bool { |
| 41 | + // Check preceding boundary |
| 42 | + if start > 0 { |
| 43 | + let preceding_char = text[..start].chars().last(); |
| 44 | + if let Some(c) = preceding_char { |
| 45 | + if c.is_alphanumeric() || c == '_' { |
| 46 | + return false; |
| 47 | + } |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + // Check following boundary |
| 52 | + if end < text.len() { |
| 53 | + let following_char = text[end..].chars().next(); |
| 54 | + if let Some(c) = following_char { |
| 55 | + if c.is_alphanumeric() || c == '_' || c == '-' { |
| 56 | + return false; |
| 57 | + } |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + true |
| 62 | +} |
| 63 | + |
| 64 | +#[cfg(test)] |
| 65 | +mod tests { |
| 66 | + use super::*; |
| 67 | + |
| 68 | + #[test] |
| 69 | + fn basic_mentions() { |
| 70 | + assert_eq!(suppress_github_mentions("Hello @user"), "Hello `@user`"); |
| 71 | + assert_eq!( |
| 72 | + suppress_github_mentions("Ping @developer"), |
| 73 | + "Ping `@developer`" |
| 74 | + ); |
| 75 | + assert_eq!( |
| 76 | + suppress_github_mentions("Multiple @user1 and @user2"), |
| 77 | + "Multiple `@user1` and `@user2`" |
| 78 | + ); |
| 79 | + } |
| 80 | + |
| 81 | + #[test] |
| 82 | + fn team_mentions() { |
| 83 | + assert_eq!(suppress_github_mentions("@org/team"), "`@org/team`"); |
| 84 | + assert_eq!( |
| 85 | + suppress_github_mentions("@rust-lang/libs"), |
| 86 | + "`@rust-lang/libs`" |
| 87 | + ); |
| 88 | + assert_eq!( |
| 89 | + suppress_github_mentions("@org/team/subteam"), |
| 90 | + "`@org/team/subteam`" |
| 91 | + ); |
| 92 | + } |
| 93 | + |
| 94 | + #[test] |
| 95 | + fn mention_boundaries() { |
| 96 | + // Adjacent punctuation |
| 97 | + assert_eq!( |
| 98 | + suppress_github_mentions("Hello,@user! How are you?"), |
| 99 | + "Hello,`@user`! How are you?" |
| 100 | + ); |
| 101 | + |
| 102 | + // Email addresses |
| 103 | + assert_eq!( |
| 104 | + suppress_github_mentions ("[email protected]"), |
| 105 | + |
| 106 | + ); |
| 107 | + |
| 108 | + // Invalid mentions |
| 109 | + assert_eq!(suppress_github_mentions("@-user"), "@-user"); |
| 110 | + assert_eq!(suppress_github_mentions("word@user"), "word@user"); |
| 111 | + assert_eq!(suppress_github_mentions("@user_next"), "@user_next"); |
| 112 | + } |
| 113 | + |
| 114 | + #[test] |
| 115 | + fn edge_cases() { |
| 116 | + // Empty input |
| 117 | + assert_eq!(suppress_github_mentions(""), ""); |
| 118 | + |
| 119 | + // Minimum valid mention |
| 120 | + assert_eq!(suppress_github_mentions("@a"), "`@a`"); |
| 121 | + |
| 122 | + // Maximum length mention |
| 123 | + let long_mention = "@".to_string() + &"a".repeat(39); |
| 124 | + assert_eq!( |
| 125 | + suppress_github_mentions(&long_mention), |
| 126 | + format!("`{long_mention}`") |
| 127 | + ); |
| 128 | + } |
| 129 | +} |
0 commit comments