@@ -25,7 +25,6 @@ import ( |
| 25 | 25 | "context" |
| 26 | 26 | "regexp" |
| 27 | 27 | "strconv" |
| 28 | | - "strings" |
| 29 | 28 | |
| 30 | 29 | "github.com/yuin/goldmark" |
| 31 | 30 | "github.com/yuin/goldmark/ast" |
@@ -93,8 +92,11 @@ type Mention struct { |
| 93 | 92 | // #6 commit prefix |
| 94 | 93 | // #7 emoji name |
| 95 | 94 | var reCombined = regexp.MustCompile(`` + |
| 96 | | - // cross-repo: alice/proj#3 |
| 97 | | - `([A-Za-z0-9][A-Za-z0-9._-]*)/([A-Za-z0-9][A-Za-z0-9._-]*)#([0-9]{1,9})\b` + |
| 95 | + // cross-repo: alice/proj#3 — left boundary required so we don't |
| 96 | + // chew into a preceding word (e.g. `xfoo/bar#3` should not be a |
| 97 | + // match of `foo/bar#3`). Mirrors the same-repo / mention shape; |
| 98 | + // the audit caught the asymmetry (S00-S25, M). |
| 99 | + `(?:^|[^\w/])([A-Za-z0-9][A-Za-z0-9._-]*)/([A-Za-z0-9][A-Za-z0-9._-]*)#([0-9]{1,9})\b` + |
| 98 | 100 | // or same-repo: #3 — must have non-word non-/ boundary on the left |
| 99 | 101 | `|(?:^|[^\w/])#([0-9]{1,9})\b` + |
| 100 | 102 | // or mention: @alice — must have non-word boundary on the left |
@@ -176,6 +178,9 @@ func (t *transformer) replaceText(txt *ast.Text, source []byte) { |
| 176 | 178 | var contentStart int |
| 177 | 179 | switch { |
| 178 | 180 | case isCrossRepo: |
| 181 | + // m[2] is the owner-start position (after the regex's |
| 182 | + // leading non-word boundary char). Same shape as same-repo |
| 183 | + // — emit the boundary char as plain text, then the link. |
| 179 | 184 | contentStart = m[2] |
| 180 | 185 | case isSameRepo: |
| 181 | 186 | contentStart = m[8] - 1 // include `#` |
@@ -335,21 +340,3 @@ func (t *transformer) appendCommitLink(parent, before ast.Node, shaPrefix string |
| 335 | 340 | return true |
| 336 | 341 | } |
| 337 | 342 | |
| 338 | | -// trimLeadingNonWord drops the leading boundary char(s) — used when |
| 339 | | -// a same-repo or mention token is rendered as plain text fallback. |
| 340 | | -func trimLeadingNonWord(b []byte) []byte { |
| 341 | | - for len(b) > 0 && !isWordByte(b[0]) { |
| 342 | | - b = b[1:] |
| 343 | | - } |
| 344 | | - return b |
| 345 | | -} |
| 346 | | - |
| 347 | | -func isWordByte(c byte) bool { |
| 348 | | - return c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') |
| 349 | | -} |
| 350 | | - |
| 351 | | -// silence unused-import warnings in a stripped build. |
| 352 | | -var ( |
| 353 | | - _ = strings.Builder{} |
| 354 | | - _ = trimLeadingNonWord |
| 355 | | -) |