Skip to content

Commit 37b9490

Browse files
committed
Added @mention support.
1 parent a75944f commit 37b9490

File tree

3 files changed

+105
-19
lines changed

3 files changed

+105
-19
lines changed

README.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,29 @@ This extension implements shorthand to specify links to GitHub in various ways.
6363
All links in the generated HTML are assigned a `gh-link` class as well as a class
6464
unique to that type of link. See each type for the specific class assigned.
6565

66-
### Users
66+
### Mentions
6767

68-
This feature is *not yet implemented*.
68+
Link directly to a GitHub user, organization or project. Note that no
69+
verification is made that an actual user, organization or project exists. As the
70+
syntax does not differentiate between users and organizations, all organizations
71+
are assumed to be users. However, this assumption is only reflected in the
72+
title of a link.
6973

70-
### Projects
74+
Mentions use the format `@{user}` to link to a user or organization and
75+
`@{user}/{project}` to link to a project. The defaults defined in the
76+
configuration options are ignored by mentions. An mention may be escaped by
77+
adding a backslash immediately before the at sign (`@`).
7178

72-
This feature is *not yet implemented*.
79+
All mentions are assigned the `gh-mention` class.
80+
81+
The following table provides some examples:
82+
83+
| shorthand | href | rendered result |
84+
| ---------- | -----------------------------| ------------------------------------------------------------------|
85+
| `@foo` | `https://github.com/foo` | [@foo](https://github.com/foo "GitHub User: @foo") |
86+
| `@foo/bar` | `https://github.com/foo/bar` | [@foo/bar](https://github.com/foo/bar "GitHub Project: @foo/bar") |
87+
| `\@123` | | @foo |
88+
| `\@foo/bar | | @foo/bar |
7389

7490
### Issues
7591

@@ -81,19 +97,19 @@ properly redirect an issue URL to a PR URL if appropriate.
8197
Issue links use the format `#{num}` or `{user}/{project}#{num}`. `{num}` is the
8298
number assigned to the issue or PR. `{user}` and `{project}` will use the
8399
defaults defined in the configuration options if not provided. An issue link may
84-
be escaped by adding a backslash immediately before the hash mark.
100+
be escaped by adding a backslash immediately before the hash mark (`#`).
85101

86102
All issue links are assigned the `gh-issue` class.
87103

88104
The following table provides various examples (with the defaults set as
89105
`user='user', project='project'`):
90106

91-
| shorthand | href | rendered result |
92-
| ----------------- | -------------------------------------------- | ----------------------------------------------------------------------------------- |
93-
| `#123` | `https://github.com/user/project/issues/123` | [#123](https://github.com/user/project/issues/123 "GitHub Issue user/project #123") |
94-
| `foo/bar#123` | `https://github.com/foo/bar/issues/123` | [foo/bar#123](https://github.com/foo/bar/issues/123 "GitHub Issue foo/bar #123") |
95-
| `\#123` (escaped) | | #123 |
96-
| `foo/bar\#123` | | foo/bar#123 |
107+
| shorthand | href | rendered result |
108+
| -------------- | -------------------------------------------- | ----------------------------------------------------------------------------------- |
109+
| `#123` | `https://github.com/user/project/issues/123` | [#123](https://github.com/user/project/issues/123 "GitHub Issue user/project #123") |
110+
| `foo/bar#123` | `https://github.com/foo/bar/issues/123` | [foo/bar#123](https://github.com/foo/bar/issues/123 "GitHub Issue foo/bar #123") |
111+
| `\#123` | | #123 |
112+
| `foo/bar\#123` | | foo/bar#123 |
97113

98114
### Commits
99115

mdx_gh_links.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,38 @@
3838
URL_BASE = 'https://github.com'
3939
RE_PARTS = dict(
4040
USER=r'[-_\w]+',
41-
PROJECT=r'[-_.\w]+'
41+
PROJECT=r'[-_.\w]+\b'
4242
)
4343

4444

45+
def _build_link(label, title, href, classes):
46+
el = etree.Element('a')
47+
el.text = label
48+
el.set('title', title)
49+
el.set('href', href)
50+
el.set('class', classes)
51+
return el
52+
53+
54+
class MentionPattern(Pattern):
55+
def __init__(self, config, md):
56+
MENTION_RE = r'(@({USER})(?:\/({PROJECT}))?)'.format(**RE_PARTS)
57+
super(MentionPattern, self).__init__(MENTION_RE, md)
58+
self.config = config
59+
60+
def handleMatch(self, m):
61+
label = m.group(2)
62+
user = m.group(3)
63+
project = m.group(4)
64+
if project:
65+
title = 'GitHub Project: @{0}/{1}'.format(user, project)
66+
href = '{0}/{1}/{2}'.format(URL_BASE, user, project)
67+
else:
68+
title = 'GitHub User: @{0}'.format(user)
69+
href = '{0}/{1}'.format(URL_BASE, user)
70+
return _build_link(label, title, href, 'gh-link gh-mention')
71+
72+
4573
class IssuePattern(Pattern):
4674
def __init__(self, config, md):
4775
ISSUE_RE = r'((?:({USER})\/({PROJECT}))?#([0-9]+))'.format(**RE_PARTS)
@@ -53,15 +81,9 @@ def handleMatch(self, m):
5381
user = m.group(3) or self.config['user']
5482
project = m.group(4) or self.config['project']
5583
num = m.group(5).lstrip('0')
56-
57-
el = etree.Element('a')
58-
el.text = label
5984
title = 'GitHub Issue {0}/{1} #{2}'.format(user, project, num)
60-
el.set('title', title)
6185
href = '{0}/{1}/{2}/issues/{3}'.format(URL_BASE, user, project, num)
62-
el.set('href', href)
63-
el.set('class', 'gh-link gh-issue')
64-
return el
86+
return _build_link(label, title, href, 'gh-link gh-issue')
6587

6688

6789
class GithubLinks(Extension):
@@ -73,7 +95,9 @@ def __init__(self, *args, **kwargs):
7395
super(GithubLinks, self).__init__(*args, **kwargs)
7496

7597
def extendMarkdown(self, md, md_globals):
98+
md.ESCAPED_CHARS.append('@')
7699
md.inlinePatterns['issue'] = IssuePattern(self.getConfigs(), md)
100+
md.inlinePatterns['mention'] = MentionPattern(self.getConfigs(), md)
77101

78102

79103
def makeExtension(*args, **kwargs):

test_gh_links.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def assertMarkdownRenders(self, source, expected, **kwargs):
4545
output = markdown(source, extensions=[GithubLinks(**configs)])
4646
self.assertMultiLineEqual(output, expected)
4747

48+
# Issue Tests
4849
def test_issue(self):
4950
self.assertMarkdownRenders(
5051
'Issue #123.',
@@ -101,6 +102,51 @@ def test_escaped_issue_with_project(self):
101102
'<p>Issue Organization/Project#123.</p>',
102103
)
103104

105+
# Mention Tests
106+
def test_mention_user(self):
107+
self.assertMarkdownRenders(
108+
'User @foo.',
109+
'<p>User <a class="gh-link gh-mention" '
110+
'href="https://github.com/foo" '
111+
'title="GitHub User: @foo">@foo</a>.</p>',
112+
)
113+
114+
def test_mention_complex_user(self):
115+
self.assertMarkdownRenders(
116+
'User @Foo_Bar-42.',
117+
'<p>User <a class="gh-link gh-mention" '
118+
'href="https://github.com/Foo_Bar-42" '
119+
'title="GitHub User: @Foo_Bar-42">@Foo_Bar-42</a>.</p>',
120+
)
121+
122+
def test_escape_mention_user(self):
123+
self.assertMarkdownRenders(
124+
'User \@foo.',
125+
'<p>User @foo.</p>',
126+
)
127+
128+
def test_mention_project(self):
129+
self.assertMarkdownRenders(
130+
'User @foo/bar.',
131+
'<p>User <a class="gh-link gh-mention" '
132+
'href="https://github.com/foo/bar" '
133+
'title="GitHub Project: @foo/bar">@foo/bar</a>.</p>',
134+
)
135+
136+
def test_mention_project_complex(self):
137+
self.assertMarkdownRenders(
138+
'User @foo/bar_baz-42.0.',
139+
'<p>User <a class="gh-link gh-mention" '
140+
'href="https://github.com/foo/bar_baz-42.0" '
141+
'title="GitHub Project: @foo/bar_baz-42.0">@foo/bar_baz-42.0</a>.</p>',
142+
)
143+
144+
def test_escape_mention_project(self):
145+
self.assertMarkdownRenders(
146+
'User \@foo/bar.',
147+
'<p>User @foo/bar.</p>',
148+
)
149+
104150

105151
if __name__ == '__main__':
106152
unittest.main()

0 commit comments

Comments
 (0)