Repository: facelessuser/pymdown-extensions

Issue:
Add magiclink support for private GitHub / GitLab url
Using magiclink with a custom GitLab or GitHub URL (ex: private repo).

Please allow customization of the `url` parameter in `pymdown-extensions/pymdownx/magiclink.py`

Discussion:
facelessuser: @gir-bot lgtm

Patch:
diff --git a/docs/src/dictionary/en-custom.txt b/docs/src/dictionary/en-custom.txt
index 9ddcf2b1..8e2544de 100644
--- a/docs/src/dictionary/en-custom.txt
+++ b/docs/src/dictionary/en-custom.txt
@@ -263,6 +263,7 @@ syntaxes
 th
 theming
 thumbsup
+tooltips
 tox
 uc
 un
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 58a73475..e0c35421 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,9 +1,13 @@
 # Changelog
 
+## 10.6
+
+-   **NEW**: MagicLink: Allow configuring custom repository providers based off the existing providers.
+
 ## 10.5
 
-- **NEW**: Blocks: Admonitions and Details now allow configuring custom block classes and default titles.
-- **FIX**: Keys: Ensure that Keys does not parse base64 encoded URLs.
+-   **NEW**: Blocks: Admonitions and Details now allow configuring custom block classes and default titles.
+-   **FIX**: Keys: Ensure that Keys does not parse base64 encoded URLs.
 
 ## 10.4
 
diff --git a/docs/src/markdown/extensions/magiclink.md b/docs/src/markdown/extensions/magiclink.md
index e1fac358..d4cc7182 100644
--- a/docs/src/markdown/extensions/magiclink.md
+++ b/docs/src/markdown/extensions/magiclink.md
@@ -445,6 +445,40 @@ repository name. You can override them and add more via the option [`shortener_u
 MagicLink added user name and repository name link shortening along.
 ///
 
+## Custom Repository Hosts
+
+It is possible that someone may be running their own private GitHub, GitLab, or Bitbucket repository. MagicLink allows
+for the creating variants of either of these repository providers with a custom host. MagicLink provides no additional
+control over specifics, so if this is sufficient for your needs, then it give it a try!
+
+To specify a custom provider, you simply need to specify them via the `custom` option.
+
+1. Simply specify the name to identify the provider (must only contain alphanumeric characters). Provider name is used
+   when manually specifying a provider (`@provider:user`) and will be used to generate custom CSS classes
+   `magiclink-provider`.
+2. Specify the `type`. Is this a private `github`, `gitlab`, or `bitbucket` provider.
+3. Specify the `label` for tooltips.
+4. Specify the `host` for your private repository.
+
+```js
+'custom': {
+    'test': {
+        'host': 'http://test.com',
+        'label': 'Test',
+        'type': 'github'
+    }
+}
+```
+
+Host URLs assume the `www` subdomain, and will generate the URL pattern to capture explicit or implicit `www` in host
+URLs, whether you specify it in the host URL or not. If your repository does not use `www` subdomain, then set the
+option `www` to `#!py False`. Most people will never need to touch this.
+
+Lastly, `shortener_user_exclude` will assume your custom provider requires the same exclude list of the specified `type`
+and will copy them for your custom repository. If this is not sufficient, you can add an entry to
+`shortener_user_exclude` for your custom repository provider using your specified `name`. If you manually set excludes
+in this manner, no excludes from the same `type` will be copied over.
+
 ## CSS
 
 For normal links, no classes are added to the anchor tags. For repository links, `magiclink` will be added as a class.
diff --git a/pymdownx/__meta__.py b/pymdownx/__meta__.py
index 4d3dd919..99d48b0b 100644
--- a/pymdownx/__meta__.py
+++ b/pymdownx/__meta__.py
@@ -185,5 +185,5 @@ def parse_version(ver, pre=False):
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(10, 5, 0, "final")
+__version_info__ = Version(10, 6, 0, "final")
 __version__ = __version_info__._get_canonical()
diff --git a/pymdownx/magiclink.py b/pymdownx/magiclink.py
index 4174d731..dc1199ea 100644
--- a/pymdownx/magiclink.py
+++ b/pymdownx/magiclink.py
@@ -64,6 +64,8 @@ RE_LINK = r'''(?xi)
 
 RE_AUTOLINK = r'(?i)<((?:ht|f)tps?://[^<>]*)>'
 
+RE_CUSTOM_NAME = re.compile(r'^[a-zA-Z0-9]+$')
+
 # Provider specific user regex rules
 RE_TWITTER_USER = r'\w{1,15}'
 RE_GITHUB_USER = r'[a-zA-Z\d](?:[-a-zA-Z\d_]{0,37}[a-zA-Z\d])?'
@@ -77,14 +79,25 @@ RE_ALL_EXT_MENTIONS = r'''(?x)
     (?:{})
 )\b
 '''
-RE_TWITTER_EXT_MENTIONS = r'twitter:{}'.format(RE_TWITTER_USER)
-RE_GITHUB_EXT_MENTIONS = r'github:{}'.format(RE_GITHUB_USER)
-RE_GITLAB_EXT_MENTIONS = r'gitlab:{}'.format(RE_GITLAB_USER)
-RE_BITBUCKET_EXT_MENTIONS = r'bitbucket:{}'.format(RE_BITBUCKET_USER)
 
 # Internal mention patterns
 RE_INT_MENTIONS = r'(?P<mention>(?<![a-zA-Z])@{})\b'
 
+def create_ext_mentions(name, provider_type):
+    """Create external mentions by provider type."""
+
+    if provider_type == 'github':
+        return r'{}:{}'.format(name, RE_GITHUB_USER)
+    elif provider_type == 'gitlab':
+        return r'{}:{}'.format(name, RE_GITLAB_USER)
+    elif provider_type == 'bitbucket':
+        return r'{}:{}'.format(name, RE_BITBUCKET_USER)
+
+RE_TWITTER_EXT_MENTIONS = r'twitter:{}'.format(RE_TWITTER_USER)
+RE_GITHUB_EXT_MENTIONS = create_ext_mentions('github', 'github')
+RE_GITLAB_EXT_MENTIONS = create_ext_mentions('gitlab', 'gitlab')
+RE_BITBUCKET_EXT_MENTIONS = create_ext_mentions('bitbucket', 'bitbucket')
+
 # External repo mention patterns
 RE_GIT_EXT_REPO_MENTIONS = r'''(?x)
 (?P<mention>
@@ -92,7 +105,7 @@ RE_GIT_EXT_REPO_MENTIONS = r'''(?x)
     @(?:{})
 )\b
 /(?P<mention_repo>[-._a-zA-Z\d]{{0,99}}[a-zA-Z\d])\b
-'''.format('|'.join([RE_GITHUB_EXT_MENTIONS, RE_GITLAB_EXT_MENTIONS, RE_BITBUCKET_EXT_MENTIONS]))
+'''
 
 # Internal repo mention patterns
 RE_GIT_INT_REPO_MENTIONS = r'''(?x)
@@ -105,7 +118,7 @@ RE_GIT_EXT_REFS = r'''(?x)
 (?P<all>(?<![@/])(?:(?P<user>\b{})/)
 (?P<repo>\b[-._a-zA-Z\d]{{0,99}}[a-zA-Z\d])
 (?:(?P<issue>(?:\#|!|\?)[1-9][0-9]*)|(?P<commit>@[a-f\d]{{40}})(?:\.{{3}}(?P<diff>[a-f\d]{{40}}))?))\b
-'''.format('|'.join([RE_GITHUB_EXT_MENTIONS, RE_GITLAB_EXT_MENTIONS, RE_BITBUCKET_EXT_MENTIONS]))
+'''
 
 # Internal reference patterns (issue, pull request, commit, compare)
 RE_GIT_INT_EXT_REFS = r'''(?x)
@@ -121,50 +134,112 @@ RE_GIT_INT_MICRO_REFS = r'''(?x)
 )\b
 '''
 
+RE_WWW = re.compile(r'(https?://)(?:www\\\.)?(.*)')
+
+REPO_LINK_TEMPLATES = {
+    'github': (
+        r'''
+        (?P<github>(?P<github_base>{}/
+        (?P<github_user_repo>(?P<github_user>{})/[^/]+))/
+            (?:issues/(?P<github_issue>\d+)/?|
+                pull/(?P<github_pull>\d+)/?|
+                discussions/(?P<github_discuss>\d+)/?|
+                commit/(?P<github_commit>[\da-f]{{7,40}})/?|
+                compare/(?P<github_diff1>[\da-f]{{7,40}})\.{{3}}
+                    (?P<github_diff2>[\da-f]{{7,40}})))''',
+        RE_GITHUB_USER
+    ),
+    'bitbucket': (
+        r'''
+        (?P<bitbucket>(?P<bitbucket_base>{}/
+        (?P<bitbucket_user_repo>(?P<bitbucket_user>{})/[^/]+))/
+            (?:issues/(?P<bitbucket_issue>\d+)(?:/[^/]+)?/?|
+                pull-requests/(?P<bitbucket_pull>\d+)(?:/[^/]+(?:/diff)?)?/?|
+                commits/commit/(?P<bitbucket_commit>[\da-f]{{7,40}})/?|
+                branches/commits/(?P<bitbucket_diff1>[\da-f]{{7,40}})
+                    (?:\.{{2}}|%0d)(?P<bitbucket_diff2>[\da-f]{{7,40}})\#diff))''',
+        RE_BITBUCKET_USER
+    ),
+    'gitlab': (
+        r'''
+        (?P<gitlab>(?P<gitlab_base>{}/
+        (?P<gitlab_user_repo>(?P<gitlab_user>{})/[^/]+))/(?:-/)?
+            (?:issues/(?P<gitlab_issue>\d+)/?|
+                merge_requests/(?P<gitlab_pull>\d+)/?|
+                commit/(?P<gitlab_commit>[\da-f]{{8,40}})/?|
+                compare/(?P<gitlab_diff1>[\da-f]{{8,40}})\.{{3}}
+                    (?P<gitlab_diff2>[\da-f]{{8,40}})))''',
+        RE_GITLAB_USER
+    )
+}
+
+
+def create_repo_link_pattern(provider, host, www=True):
+    """Create repository link provider."""
+
+    template = REPO_LINK_TEMPLATES[provider]
+    host_pat = re.escape(host.lower().rstrip('/'))
+    if www:
+        m = RE_WWW.match(host_pat)
+        if m:
+            host_pat = m.group(1) + r'(?:w{3}\.)?' + m.group(2)
+    return template[0].format(host_pat, template[1])
+
+
 # Repository link shortening pattern
 RE_REPO_LINK = re.compile(
-    r'''(?xi)
-    ^(?:
-        (?P<github>(?P<github_base>https://(?:w{{3}}\.)?github\.com/
-            (?P<github_user_repo>(?P<github_user>{})/[^/]+))/
-                (?:issues/(?P<github_issue>\d+)/?|
-                    pull/(?P<github_pull>\d+)/?|
-                    discussions/(?P<github_discuss>\d+)/?|
-                    commit/(?P<github_commit>[\da-f]{{7,40}})/?|
-                    compare/(?P<github_diff1>[\da-f]{{7,40}})\.{{3}}
-                        (?P<github_diff2>[\da-f]{{7,40}})))|
-
-        (?P<bitbucket>(?P<bitbucket_base>https://(?:w{{3}}\.)?bitbucket\.org/
-            (?P<bitbucket_user_repo>(?P<bitbucket_user>{})/[^/]+))/
-                (?:issues/(?P<bitbucket_issue>\d+)(?:/[^/]+)?/?|
-                    pull-requests/(?P<bitbucket_pull>\d+)(?:/[^/]+(?:/diff)?)?/?|
-                    commits/commit/(?P<bitbucket_commit>[\da-f]{{7,40}})/?|
-                    branches/commits/(?P<bitbucket_diff1>[\da-f]{{7,40}})
-                        (?:\.{{2}}|%0d)(?P<bitbucket_diff2>[\da-f]{{7,40}})\#diff))|
-
-        (?P<gitlab>(?P<gitlab_base>https://(?:w{{3}}\.)?gitlab\.com/
-            (?P<gitlab_user_repo>(?P<gitlab_user>{})/[^/]+))/(?:-/)?
-                (?:issues/(?P<gitlab_issue>\d+)/?|
-                    merge_requests/(?P<gitlab_pull>\d+)/?|
-                    commit/(?P<gitlab_commit>[\da-f]{{8,40}})/?|
-                    compare/(?P<gitlab_diff1>[\da-f]{{8,40}})\.{{3}}
-                        (?P<gitlab_diff2>[\da-f]{{8,40}})))
-    )/?$
-    '''.format(RE_GITHUB_USER, RE_BITBUCKET_USER, RE_GITLAB_USER)
+    r'''(?xi)^(?:{}|{}|{})/?$'''.format(
+        create_repo_link_pattern('github', "https://github.com"),
+        create_repo_link_pattern('bitbucket', "https://bitbucket.org"),
+        create_repo_link_pattern('gitlab', 'https://gitlab.com'),
+    )
 )
 
+
+USER_LINK_TEMPLATES = {
+    'github': (
+        r'''
+        (?P<github>(?P<github_base>{}/
+            (?P<github_user_repo>(?P<github_user>{})(?:/(?P<github_repo>[^/]+))?)))
+        ''',
+        RE_GITHUB_USER
+    ),
+    'bitbucket': (
+        r'''
+        (?P<bitbucket>(?P<bitbucket_base>{}/
+            (?P<bitbucket_user_repo>(?P<bitbucket_user>{})(?:/(?P<bitbucket_repo>[^/]+)/?)?)))
+        ''',
+        RE_BITBUCKET_USER
+    ),
+    'gitlab': (
+        r'''
+        (?P<gitlab>(?P<gitlab_base>{}/
+            (?P<gitlab_user_repo>(?P<gitlab_user>{})(?:/(?P<gitlab_repo>[^/]+))?)))
+        ''',
+        RE_GITLAB_USER
+    )
+}
+
+
+def create_user_link_pattern(provider, host, www=True):
+    """Create repository link provider."""
+
+    template = USER_LINK_TEMPLATES[provider]
+    host_pat = re.escape(host.lower().rstrip('/'))
+    if www:
+        m = RE_WWW.match(host_pat)
+        if m:
+            host_pat = m.group(1) + r'(?:w{3}\.)?' + m.group(2)
+    return template[0].format(host_pat, template[1])
+
+
 # Repository link shortening pattern
 RE_USER_REPO_LINK = re.compile(
-    r'''(?xi)
-    ^(?:
-        (?P<github>(?P<github_base>https://(?:w{{3}}\.)?github\.com/
-            (?P<github_user_repo>(?P<github_user>{})(?:/(?P<github_repo>[^/]+))?))) |
-        (?P<bitbucket>(?P<bitbucket_base>https://(?:w{{3}}\.)?bitbucket\.org/
-            (?P<bitbucket_user_repo>(?P<bitbucket_user>{})(?:/(?P<bitbucket_repo>[^/]+)/?)?))) |
-        (?P<gitlab>(?P<gitlab_base>https://(?:w{{3}}\.)?gitlab\.com/
-            (?P<gitlab_user_repo>(?P<gitlab_user>{})(?:/(?P<gitlab_repo>[^/]+))?))) |
-    )/?$
-    '''.format(RE_GITHUB_USER, RE_BITBUCKET_USER, RE_GITLAB_USER)
+    r'''(?xi)^(?:{}|{}|{})/?$'''.format(
+        create_user_link_pattern('github', 'https://github.com'),
+        create_user_link_pattern('bitbucket', '"https://bitbucket.org"'),
+        create_user_link_pattern('gitlab', 'https://gitlab.com')
+    )
 )
 
 RE_SOCIAL_LINK = re.compile(
@@ -177,57 +252,83 @@ RE_SOCIAL_LINK = re.compile(
 
 # Provider specific info (links, names, specific patterns, etc.)
 SOCIAL_PROVIDERS = {'twitter'}
-PROVIDER_INFO = {
-    "twitter": {
-        "provider": "Twitter",
-        "url": "https://twitter.com",
-        "user_pattern": RE_TWITTER_USER
-    },
+
+# Templates for providers
+PROVIDER_TEMPLATES = {
     "gitlab": {
         "provider": "GitLab",
-        "url": "https://gitlab.com",
+        "type": "gitlab",
+        "url": "{}",
         "user_pattern": RE_GITLAB_USER,
-        "issue": "https://gitlab.com/{}/{}/-/issues/{}",
-        "pull": "https://gitlab.com/{}/{}/-/merge_requests/{}",
-        "commit": "https://gitlab.com/{}/{}/-/commit/{}",
-        "compare": "https://gitlab.com/{}/{}/-/compare/{}...{}",
+        "issue": "{}/{{}}/{{}}/-/issues/{{}}",
+        "pull": "{}/{{}}/{{}}/-/merge_requests/{{}}",
+        "commit": "{}/{{}}/{{}}/-/commit/{{}}",
+        "compare": "{}/{{}}/{{}}/-/compare/{{}}...{{}}",
         "hash_size": 8
     },
     "bitbucket": {
         "provider": "Bitbucket",
-        "url": "https://bitbucket.org",
+        "type": "bitbucket",
+        "url": "{}",
         "user_pattern": RE_BITBUCKET_USER,
-        "issue": "https://bitbucket.org/{}/{}/issues/{}",
-        "pull": "https://bitbucket.org/{}/{}/pull-requests/{}",
-        "commit": "https://bitbucket.org/{}/{}/commits/commit/{}",
-        "compare": "https://bitbucket.org/{}/{}/branches/commits/{}..{}#diff",
+        "issue": "{}/{{}}/{{}}/issues/{{}}",
+        "pull": "{}/{{}}/{{}}/pull-requests/{{}}",
+        "commit": "{}/{{}}/{{}}/commits/commit/{{}}",
+        "compare": "{}/{{}}/{{}}/branches/commits/{{}}..{{}}#diff",
         "hash_size": 7
     },
     "github": {
         "provider": "GitHub",
-        "url": "https://github.com",
+        "type": "github",
+        "url": "{}",
         "user_pattern": RE_GITHUB_USER,
-        "issue": "https://github.com/{}/{}/issues/{}",
-        "pull": "https://github.com/{}/{}/pull/{}",
-        "discuss": 'https://github.com/{}/{}/discussions/{}',
-        "commit": "https://github.com/{}/{}/commit/{}",
-        "compare": "https://github.com/{}/{}/compare/{}...{}",
+        "issue": "{}/{{}}/{{}}/issues/{{}}",
+        "pull": "{}/{{}}/{{}}/pull/{{}}",
+        "discuss": '{}/{{}}/{{}}/discussions/{{}}',
+        "commit": "{}/{{}}/{{}}/commit/{{}}",
+        "compare": "{}/{{}}/{{}}/compare/{{}}...{{}}",
         "hash_size": 7
-    }
+    },
+    "twitter": {
+        "provider": "Twitter",
+        "type": "twitter",
+        "url": "{}",
+        "user_pattern": RE_TWITTER_USER
+    },
+}
+
+
+def create_provider(provider, host):
+    """Create the provider with the provided host."""
+
+    entry = PROVIDER_TEMPLATES[provider].copy()
+    for key in ('url', 'issue', 'pull', 'commit', 'compare', 'discuss'):
+        if key not in entry:
+            continue
+        entry[key] = entry[key].format(host.lower().rstrip('/'))
+    return entry
+
+
+PROVIDER_INFO = {
+    "twitter": create_provider('twitter', "https://twitter.com"),
+    "gitlab": create_provider('gitlab', 'https://gitlab.com'),
+    "bitbucket": create_provider('bitbucket', "https://bitbucket.org"),
+    "github": create_provider('github', "https://github.com")
 }
 
 
 class _MagiclinkShorthandPattern(InlineProcessor):
     """Base shorthand link class."""
 
-    def __init__(self, pattern, md, user, repo, provider, labels, normalize):
+    def __init__(self, pattern, md, user, repo, provider, labels, normalize, provider_info):
         """Initialize."""
 
         self.user = user
         self.repo = repo
         self.labels = labels
         self.normalize = normalize
-        self.provider = provider if provider in PROVIDER_INFO else ''
+        self.provider_info = provider_info
+        self.provider = provider if provider in self.provider_info else ''
         InlineProcessor.__init__(self, pattern, md)
 
 
@@ -241,17 +342,17 @@ class _MagiclinkReferencePattern(_MagiclinkShorthandPattern):
         issue_value = issue[1:]
 
         if issue_type == '#':
-            issue_link = PROVIDER_INFO[provider]['issue']
+            issue_link = self.provider_info[provider]['issue']
             issue_label = self.labels.get('issue', 'Issue')
             class_name = 'magiclink-issue'
             icon = issue_type
         elif issue_type == '!':
-            issue_link = PROVIDER_INFO[provider]['pull']
+            issue_link = self.provider_info[provider]['pull']
             issue_label = self.labels.get('pull', 'Pull Request')
             class_name = 'magiclink-pull'
             icon = '#' if self.normalize else issue_type
-        elif provider == "github" and issue_type == '?':
-            issue_link = PROVIDER_INFO[provider]['discuss']
+        elif self.provider_info[provider]['type'] == "github" and issue_type == '?':
+            issue_link = self.provider_info[provider]['discuss']
             issue_label = self.labels.get('discuss', 'Discussion')
             class_name = 'magiclink-discussion'
             icon = '#' if self.normalize else issue_type
@@ -270,7 +371,7 @@ class _MagiclinkReferencePattern(_MagiclinkShorthandPattern):
         el.set(
             'title',
             '{} {}: {}/{} #{}'.format(
-                PROVIDER_INFO[provider]['provider'],
+                self.provider_info[provider]['provider'],
                 issue_label,
                 user,
                 repo,
@@ -282,7 +383,7 @@ class _MagiclinkReferencePattern(_MagiclinkShorthandPattern):
     def process_commit(self, el, provider, user, repo, commit):
         """Process commit."""
 
-        hash_ref = commit[0:PROVIDER_INFO[provider]['hash_size']]
+        hash_ref = commit[0:self.provider_info[provider]['hash_size']]
         if self.my_repo:
             text = hash_ref
         elif self.my_user:
@@ -290,13 +391,13 @@ class _MagiclinkReferencePattern(_MagiclinkShorthandPattern):
         else:
             text = '{}/{}@{}'.format(user, repo, hash_ref)
 
-        el.set('href', PROVIDER_INFO[provider]['commit'].format(user, repo, commit))
+        el.set('href', self.provider_info[provider]['commit'].format(user, repo, commit))
         el.text = md_util.AtomicString(text)
         el.set('class', 'magiclink magiclink-{} magiclink-commit'.format(provider))
         el.set(
             'title',
             '{} {}: {}/{}@{}'.format(
-                PROVIDER_INFO[provider]['provider'],
+                self.provider_info[provider]['provider'],
                 self.labels.get('commit', 'Commit'),
                 user,
                 repo,
@@ -307,8 +408,8 @@ class _MagiclinkReferencePattern(_MagiclinkShorthandPattern):
     def process_compare(self, el, provider, user, repo, commit1, commit2):
         """Process commit."""
 
-        hash_ref1 = commit1[0:PROVIDER_INFO[provider]['hash_size']]
-        hash_ref2 = commit2[0:PROVIDER_INFO[provider]['hash_size']]
+        hash_ref1 = commit1[0:self.provider_info[provider]['hash_size']]
+        hash_ref2 = commit2[0:self.provider_info[provider]['hash_size']]
         if self.my_repo:
             text = '{}...{}'.format(hash_ref1, hash_ref2)
         elif self.my_user:
@@ -316,13 +417,13 @@ class _MagiclinkReferencePattern(_MagiclinkShorthandPattern):
         else:
             text = '{}/{}@{}...{}'.format(user, repo, hash_ref1, hash_ref2)
 
-        el.set('href', PROVIDER_INFO[provider]['compare'].format(user, repo, commit1, commit2))
+        el.set('href', self.provider_info[provider]['compare'].format(user, repo, commit1, commit2))
         el.text = md_util.AtomicString(text)
         el.set('class', 'magiclink magiclink-{} magiclink-compare'.format(provider))
         el.set(
             'title',
             '{} {}: {}/{}@{}...{}'.format(
-                PROVIDER_INFO[provider]['provider'],
+                self.provider_info[provider]['provider'],
                 self.labels.get('compare', 'Compare'),
                 user,
                 repo,
@@ -345,17 +446,30 @@ class MagicShortenerTreeprocessor(Treeprocessor):
     USER = 6
 
     def __init__(
-        self, md, base_url, base_user_url, labels, normalize, repo_shortner, social_shortener, excludes, provider
+        self,
+        md,
+        base_url,
+        base_user_url,
+        labels,
+        normalize,
+        repo_shortner,
+        social_shortener,
+        custom_shortners,
+        excludes,
+        provider,
+        provider_info
     ):
         """Initialize."""
 
         self.base = base_url
         self.repo_shortner = repo_shortner
         self.social_shortener = social_shortener
+        self.custom_shortners = custom_shortners
         self.base_user = base_user_url
         self.repo_labels = labels
         self.normalize = normalize
         self.provider = provider
+        self.provider_info = provider_info
         self.labels = {
             "github": "GitHub",
             "bitbucket": "Bitbucket",
@@ -442,6 +556,7 @@ class MagicShortenerTreeprocessor(Treeprocessor):
         """Shorten issue/pull link."""
 
         # user/repo#(issue|pull)
+        provider_type = self.provider_info[provider]['type']
         if link_type == self.ISSUE:
             issue_type = self.repo_labels.get('issue', 'Issue')
             icon = '#'
@@ -452,7 +567,7 @@ class MagicShortenerTreeprocessor(Treeprocessor):
             icon = '#' if self.normalize else '!'
             if 'magiclink-pull' not in class_name:
                 class_name.append('magiclink-pull')
-        elif provider == 'github' and link_type == self.DISCUSS:
+        elif provider_type == 'github' and link_type == self.DISCUSS:
             issue_type = self.repo_labels.get('discuss', 'Discussion')
             icon = '#' if self.normalize else '?'
             if 'magiclink-discussion' not in class_name:
@@ -470,7 +585,7 @@ class MagicShortenerTreeprocessor(Treeprocessor):
     def shorten_issue_commit(self, link, provider, link_type, user_repo, value, hash_size):
         """Shorten URL."""
 
-        label = PROVIDER_INFO[provider]['provider']
+        label = self.provider_info[provider]['provider']
         prov_class = 'magiclink-{}'.format(provider)
         class_attr = link.get('class', '')
         class_name = class_attr.split(' ') if class_attr else []
@@ -493,7 +608,7 @@ class MagicShortenerTreeprocessor(Treeprocessor):
     def shorten_user_repo(self, link, provider, link_type, user_repo):
         """Shorten URL."""
 
-        label = PROVIDER_INFO[provider]['provider']
+        label = self.provider_info[provider]['provider']
         prov_class = 'magiclink-{}'.format(provider)
         class_attr = link.get('class', '')
         class_name = class_attr.split(' ') if class_attr else []
@@ -511,7 +626,7 @@ class MagicShortenerTreeprocessor(Treeprocessor):
             self.shorten_user(link, class_name, label, user_repo)
         link.set('class', ' '.join(class_name))
 
-    def get_provider(self, match):
+    def get_provider_type(self, match):
         """Get the provider and hash size."""
 
         # Set provider specific variables
@@ -565,21 +680,21 @@ class MagicShortenerTreeprocessor(Treeprocessor):
                 link_type = self.USER
         return value, link_type
 
-    def is_my_repo(self, provider, match):
+    def is_my_repo(self, provider_type, match):
         """Check if link is from our specified user and repo."""
 
         # See if these links are from the specified repo.
-        return self.base and match.group(provider + '_base') + '/' == self.base
+        return self.base and match.group(provider_type + '_base') + '/' == self.base
 
-    def is_my_user(self, provider, match):
+    def is_my_user(self, provider_type, match):
         """Check if link is from our specified user."""
 
-        return self.base_user and match.group(provider + '_base').startswith(self.base_user)
+        return self.base_user and match.group(provider_type + '_base').startswith(self.base_user)
 
-    def excluded(self, provider, match):
+    def excluded(self, provider_type, provider, match):
         """Check if user has been excluded."""
 
-        user = match.group(provider + '_user')
+        user = match.group(provider_type + '_user')
         return user.lower() in self.excludes.get(provider, set())
 
     def run(self, root):
@@ -608,39 +723,81 @@ class MagicShortenerTreeprocessor(Treeprocessor):
                 if self.repo_shortner:
                     m = RE_REPO_LINK.match(href)
                     if m:
-                        provider = self.get_provider(m)
-                        self.my_repo = self.is_my_repo(provider, m)
-                        self.my_user = self.my_repo or self.is_my_user(provider, m)
-                        value, link_type = self.get_type(provider, m)
+                        provider_type = self.get_provider_type(m)
+                        provider = provider_type
+                        self.my_repo = self.is_my_repo(provider_type, m)
+                        self.my_user = self.my_repo or self.is_my_user(provider_type, m)
+                        value, link_type = self.get_type(provider_type, m)
                         found = True
 
                         # All right, everything set, let's shorten.
-                        if not self.excluded(provider, m):
+                        if not self.excluded(provider_type, provider, m):
                             self.shorten_issue_commit(
                                 link,
                                 provider,
                                 link_type,
-                                m.group(provider + '_user_repo'),
+                                m.group(provider_type + '_user_repo'),
                                 value,
-                                PROVIDER_INFO[provider]['hash_size']
+                                self.provider_info[provider]['hash_size']
                             )
                 if not found and self.repo_shortner:
                     m = RE_USER_REPO_LINK.match(href)
                     if m:
-                        provider = self.get_provider(m)
-                        self.my_repo = self.is_my_repo(provider, m)
-                        self.my_user = self.my_repo or self.is_my_user(provider, m)
-                        value, link_type = self.get_type(provider, m)
+                        provider_type = self.get_provider_type(m)
+                        provider = provider_type
+                        self.my_repo = self.is_my_repo(provider_type, m)
+                        self.my_user = self.my_repo or self.is_my_user(provider_type, m)
+                        value, link_type = self.get_type(provider_type, m)
                         found = True
 
-                        if not self.excluded(provider, m):
+                        if not self.excluded(provider_type, provider, m):
                             # All right, everything set, let's shorten.
                             self.shorten_user_repo(
                                 link,
                                 provider,
                                 link_type,
-                                m.group(provider + '_user_repo')
+                                m.group(provider_type + '_user_repo')
                             )
+                if not found and self.custom_shortners:
+                    for custom, entry in self.custom_shortners.items():
+                        m = entry['repo'].match(href)
+                        if m:
+                            provider = custom
+                            provider_type = self.provider_info[custom]['type']
+                            self.my_repo = self.is_my_repo(provider_type, m)
+                            self.my_user = self.my_repo or self.is_my_user(provider_type, m)
+                            value, link_type = self.get_type(provider_type, m)
+                            found = True
+
+                            # All right, everything set, let's shorten.
+                            if not self.excluded(provider_type, provider, m):
+                                self.shorten_issue_commit(
+                                    link,
+                                    provider,
+                                    link_type,
+                                    m.group(provider_type + '_user_repo'),
+                                    value,
+                                    self.provider_info[provider]['hash_size']
+                                )
+                        if not found:
+                            m = entry['user'].match(href)
+                            if m:
+                                provider = custom
+                                provider_type = self.provider_info[custom]['type']
+                                self.my_repo = self.is_my_repo(provider_type, m)
+                                self.my_user = self.my_repo or self.is_my_user(provider_type, m)
+                                value, link_type = self.get_type(provider_type, m)
+                                found = True
+
+                                if not self.excluded(provider_type, provider, m):
+                                    # All right, everything set, let's shorten.
+                                    self.shorten_user_repo(
+                                        link,
+                                        provider,
+                                        link_type,
+                                        m.group(provider_type + '_user_repo')
+                                    )
+
                 if not found and self.social_shortener:
                     m = RE_SOCIAL_LINK.match(href)
                     if m:
@@ -649,7 +806,7 @@ class MagicShortenerTreeprocessor(Treeprocessor):
                         self.my_user = self.my_repo or self.is_my_user(provider, m)
                         value, link_type = self.get_type(provider, m)
 
-                        if not self.excluded(provider, m):
+                        if not self.excluded(provider, provider, m):
                             # All right, everything set, let's shorten.
                             self.shorten_user_repo(
                                 link,
@@ -740,10 +897,10 @@ class MagiclinkMentionPattern(_MagiclinkShorthandPattern):
             mention = parts[0]
 
         el = etree.Element("a")
-        el.set('href', '{}/{}'.format(PROVIDER_INFO[provider]['url'], mention))
+        el.set('href', '{}/{}'.format(self.provider_info[provider]['url'], mention))
         el.set(
             'title',
-            "{} {}: {}".format(PROVIDER_INFO[provider]['provider'], self.labels.get('mention', "User"), mention)
+            "{} {}: {}".format(self.provider_info[provider]['provider'], self.labels.get('mention', "User"), mention)
         )
         el.set('class', 'magiclink magiclink-{} magiclink-mention'.format(provider))
         el.text = md_util.AtomicString('@{}'.format(mention))
@@ -770,11 +927,11 @@ class MagiclinkRepositoryPattern(_MagiclinkShorthandPattern):
         repo = m.group('mention_repo')
 
         el = etree.Element("a")
-        el.set('href', '{}/{}/{}'.format(PROVIDER_INFO[provider]['url'], user, repo))
+        el.set('href', '{}/{}/{}'.format(self.provider_info[provider]['url'], user, repo))
         el.set(
             'title',
             "{} {}: {}/{}".format(
-                PROVIDER_INFO[provider]['provider'], self.labels.get('repository', 'Repository'), user, repo
+                self.provider_info[provider]['provider'], self.labels.get('repository', 'Repository'), user, repo
             )
         )
         el.set('class', 'magiclink magiclink-{} magiclink-repository'.format(provider))
@@ -915,6 +1072,10 @@ class MagiclinkExtension(Extension):
             'repo': [
                 '',
                 'The base repo to use - Default: ""'
+            ],
+            'custom': [
+                {},
+                "Custom repositories hosts - Default {}"
             ]
         }
         super().__init__(*args, **kwargs)
@@ -933,7 +1094,7 @@ class MagiclinkExtension(Extension):
 
         md.inlinePatterns.register(MagiclinkMailPattern(RE_MAIL, md), "magic-mail", 84.9)
 
-    def setup_shorthand(self, md, int_mentions, ext_mentions, config):
+    def setup_shorthand(self, md):
         """Setup shorthand."""
 
         # Setup URL shortener
@@ -943,51 +1104,111 @@ class MagiclinkExtension(Extension):
         # Repository shorthand
         if self.git_short:
             git_ext_repo = MagiclinkRepositoryPattern(
-                RE_GIT_EXT_REPO_MENTIONS, md, self.user, self.repo, self.provider, self.labels, self.normalize
+                self.re_git_ext_repo_mentions,
+                md,
+                self.user,
+                self.repo,
+                self.provider,
+                self.labels,
+                self.normalize,
+                self.provider_info
             )
             md.inlinePatterns.register(git_ext_repo, "magic-repo-ext-mention", 79.9)
             if not self.is_social:
                 git_int_repo = MagiclinkRepositoryPattern(
-                    RE_GIT_INT_REPO_MENTIONS.format(int_mentions), md, self.user, self.repo, self.provider,
-                    self.labels, self.normalize
+                    RE_GIT_INT_REPO_MENTIONS.format(self.int_mentions),
+                    md,
+                    self.user,
+                    self.repo,
+                    self.provider,
+                    self.labels,
+                    self.normalize,
+                    self.provider_info
                 )
                 md.inlinePatterns.register(git_int_repo, "magic-repo-int-mention", 79.8)
 
         # Mentions
-        pattern = RE_ALL_EXT_MENTIONS.format('|'.join(ext_mentions))
+        pattern = RE_ALL_EXT_MENTIONS.format('|'.join(self.ext_mentions))
         git_mention = MagiclinkMentionPattern(
-            pattern, md, self.user, self.repo, self.provider, self.labels, self.normalize
+            pattern,
+            md,
+            self.user,
+            self.repo,
+            self.provider,
+            self.labels,
+            self.normalize,
+            self.provider_info
         )
         md.inlinePatterns.register(git_mention, "magic-ext-mention", 79.7)
 
         git_mention = MagiclinkMentionPattern(
-            RE_INT_MENTIONS.format(int_mentions), md, self.user, self.repo, self.provider, self.labels, self.normalize
+            RE_INT_MENTIONS.format(self.int_mentions),
+            md,
+            self.user,
+            self.repo,
+            self.provider,
+            self.labels,
+            self.normalize,
+            self.provider_info
         )
         md.inlinePatterns.register(git_mention, "magic-int-mention", 79.6)
 
         # Other project refs
         if self.git_short:
             git_ext_refs = MagiclinkExternalRefsPattern(
-                RE_GIT_EXT_REFS, md, self.user, self.repo, self.provider, self.labels, self.normalize
+                self.re_git_ext_refs,
+                md,
+                self.user,
+                self.repo,
+                self.provider,
+                self.labels,
+                self.normalize,
+                self.provider_info
             )
             md.inlinePatterns.register(git_ext_refs, "magic-ext-refs", 79.5)
             if not self.is_social:
                 git_int_refs = MagiclinkExternalRefsPattern(
-                    RE_GIT_INT_EXT_REFS.format(int_mentions), md, self.user, self.repo, self.provider,
-                    self.labels, self.normalize
+                    RE_GIT_INT_EXT_REFS.format(self.int_mentions),
+                    md,
+                    self.user,
+                    self.repo,
+                    self.provider,
+                    self.labels,
+                    self.normalize,
+                    self.provider_info
                 )
                 md.inlinePatterns.register(git_int_refs, "magic-int-refs", 79.4)
                 git_int_micro_refs = MagiclinkInternalRefsPattern(
-                    RE_GIT_INT_MICRO_REFS, md, self.user, self.repo, self.provider, self.labels, self.normalize
+                    RE_GIT_INT_MICRO_REFS,
+                    md,
+                    self.user,
+                    self.repo,
+                    self.provider,
+                    self.labels,
+                    self.normalize,
+                    self.provider_info
                 )
                 md.inlinePatterns.register(git_int_micro_refs, "magic-int-micro-refs", 79.3)
 
-    def setup_shortener(self, md, base_url, base_user_url, config, repo_shortner, social_shortener):
+    def setup_shortener(
+        self,
+        md,
+        config
+    ):
         """Setup shortener."""
 
         shortener = MagicShortenerTreeprocessor(
-            md, base_url, base_user_url, self.labels, self.normalize, repo_shortner, social_shortener,
-            self.shortener_exclusions, self.provider
+            md,
+            self.base_url,
+            self.base_user_url,
+            self.labels,
+            self.normalize,
+            self.repo_shortner,
+            self.social_shortener,
+            self.custom_shortners,
+            self.shortener_exclusions,
+            self.provider,
+            self.provider_info
         )
         shortener.config = config
         md.treeprocessors.register(shortener, "magic-repo-shortener", 9.9)
@@ -1002,8 +1223,8 @@ class MagiclinkExtension(Extension):
             return base_url, base_user_url
 
         if self.user and self.repo:
-            base_url = '{}/{}/{}/'.format(PROVIDER_INFO[self.provider]['url'], self.user, self.repo)
-            base_user_url = '{}/{}/'.format(PROVIDER_INFO[self.provider]['url'], self.user)
+            base_url = '{}/{}/{}/'.format(self.provider_info[self.provider]['url'], self.user, self.repo)
+            base_user_url = '{}/{}/'.format(self.provider_info[self.provider]['url'], self.user)
 
         return base_url, base_user_url
 
@@ -1024,34 +1245,63 @@ class MagiclinkExtension(Extension):
         self.repo_shortner = config.get('repo_url_shortener', False)
         self.social_shortener = config.get('social_url_shortener', False)
         self.shortener_exclusions = {k: set(v) for k, v in DEFAULT_EXCLUDES.items()}
+
+        self.provider_info = PROVIDER_INFO.copy()
+        custom_provider = config.get('custom', {})
+        excludes = config.get('shortener_user_exclude', {})
+        self.custom_shortners = {}
+        external_users = [RE_GITHUB_EXT_MENTIONS, RE_GITLAB_EXT_MENTIONS, RE_BITBUCKET_EXT_MENTIONS]
+        for custom, entry in custom_provider.items():
+            if not RE_CUSTOM_NAME.match(custom):
+                raise ValueError(
+                    "Name '{}' not allowed, provider name must contain only letters and numbers".format(custom)
+                )
+            if custom not in self.provider_info:
+                self.provider_info[custom] = create_provider(entry['type'], entry['host'])
+                self.provider_info[custom]['provider'] = entry['label']
+                self.custom_shortners[custom] = {
+                    'repo': re.compile(
+                        r'(?xi)^{}/?$'.format(
+                            create_repo_link_pattern(entry['type'], entry['host'], entry.get('www', True))
+                        )
+                    ),
+                    'user': re.compile(
+                        r'(?xi)^{}/?$'.format(
+                            create_user_link_pattern(entry['type'], entry['host'], entry.get('www', True))
+                        )
+                    )
+                }
+                if custom not in excludes:
+                    excludes[custom] = excludes.get(entry['type'], [])
+                external_users.append(create_ext_mentions(custom, entry['type']))
+
+        self.re_git_ext_repo_mentions = RE_GIT_EXT_REPO_MENTIONS.format('|'.join(external_users))
+        self.re_git_ext_refs = RE_GIT_EXT_REFS.format('|'.join(external_users))
+
         for key, value in config.get('shortener_user_exclude', {}).items():
-            if key in ('github', 'bitbucket', 'gitlab', 'twitter') and isinstance(value, (list, tuple, set)):
+            if key in self.provider_info and isinstance(value, (list, tuple, set)):
                 self.shortener_exclusions[key] = {x.lower() for x in value}
 
         # Ensure valid provider
-        if self.provider not in PROVIDER_INFO:
+        if self.provider not in self.provider_info:
             self.provider = 'github'
 
-        int_mentions = None
-        ext_mentions = []
-        if self.git_short:
-            ext_mentions.extend([RE_BITBUCKET_EXT_MENTIONS, RE_GITHUB_EXT_MENTIONS, RE_GITLAB_EXT_MENTIONS])
-
-        if self.social_short:
-            ext_mentions.append(RE_TWITTER_EXT_MENTIONS)
-
-        if self.git_short or self.social_short:
-            int_mentions = PROVIDER_INFO[self.provider]['user_pattern']
-
         self.setup_autolinks(md, config)
 
         if self.git_short or self.social_short:
-            self.setup_shorthand(md, int_mentions, ext_mentions, config)
+            self.ext_mentions = []
+            if self.git_short:
+                self.ext_mentions.extend(external_users)
+
+            if self.social_short:
+                self.ext_mentions.append(RE_TWITTER_EXT_MENTIONS)
+            self.int_mentions = self.provider_info[self.provider]['user_pattern']
+            self.setup_shorthand(md)
 
         # Setup link post processor for shortening repository links
         if self.repo_shortner or self.social_shortener:
-            base_url, base_user_url = self.get_base_urls(config)
-            self.setup_shortener(md, base_url, base_user_url, config, self.repo_shortner, self.social_shortener)
+            self.base_url, self.base_user_url = self.get_base_urls(config)
+            self.setup_shortener(md, config)
 
 
 def makeExtension(*args, **kwargs):


Test patch:
diff --git a/tests/test_extensions/test_magiclink.py b/tests/test_extensions/test_magiclink.py
index 9c2e246d..872dbab6 100644
--- a/tests/test_extensions/test_magiclink.py
+++ b/tests/test_extensions/test_magiclink.py
@@ -1,5 +1,6 @@
 """Test cases for MagicLink."""
 from .. import util
+import markdown
 
 
 class TestMagicLinkShortner(util.MdCase):
@@ -127,7 +128,7 @@ class TestMagicLinkShortnerSocial(util.MdCase):
     """Test cases for social link shortener."""
 
     extension = [
-        'pymdownx.magiclink',
+        'pymdownx.magiclink'
     ]
 
     extension_configs = {
@@ -160,3 +161,182 @@ class TestMagicLinkShortnerSocial(util.MdCase):
             r'https://twitter.com/home',
             r'<p><a href="https://twitter.com/home">https://twitter.com/home</a></p>'
         )
+
+
+class TestMagicLinkCustom(util.MdCase):
+    """Test cases for custom provider."""
+
+    extension = [
+        'pymdownx.magiclink',
+        'pymdownx.saneheaders'
+    ]
+
+    extension_configs = {
+        'pymdownx.magiclink': {
+            'repo_url_shorthand': True,
+            'repo_url_shortener': True,
+            'user': 'facelessuser',
+            'repo': 'pymdown-extensions',
+            'provider': 'test',
+            'custom': {
+                'test': {
+                    'host': 'http://test.com',
+                    'label': 'Test',
+                    'type': 'github'
+                }
+            }
+        }
+    }
+
+    def test_user(self):
+        """Test user in custom repo."""
+
+        self.check_markdown(
+            '@facelessuser',
+            '<p><a class="magiclink magiclink-test magiclink-mention" href="http://test.com/facelessuser" title="Test User: facelessuser">@facelessuser</a></p>'  # noqa: E501
+        )
+
+    def test_repo(self):
+        """Test repo in custom repo."""
+
+        self.check_markdown(
+            '@facelessuser/pymdown-extensions',
+            '<p><a class="magiclink magiclink-test magiclink-repository" href="http://test.com/facelessuser/pymdown-extensions" title="Test Repository: facelessuser/pymdown-extensions">facelessuser/pymdown-extensions</a></p>'  # noqa: E501
+        )
+
+    def test_default_issue(self):
+        """Test default issue case."""
+
+        self.check_markdown(
+            '#2',
+            '<p><a class="magiclink magiclink-test magiclink-issue" href="http://test.com/facelessuser/pymdown-extensions/issues/2" title="Test Issue: facelessuser/pymdown-extensions #2">#2</a></p>'  # noqa: E501
+        )
+
+    def test_default_pull(self):
+        """Test default pull case."""
+
+        self.check_markdown(
+            '!2',
+            '<p><a class="magiclink magiclink-test magiclink-pull" href="http://test.com/facelessuser/pymdown-extensions/pull/2" title="Test Pull Request: facelessuser/pymdown-extensions #2">!2</a></p>'  # noqa: E501
+        )
+
+    def test_default_discussion(self):
+        """Test default discussion case."""
+
+        self.check_markdown(
+            '?2',
+            '<p><a class="magiclink magiclink-test magiclink-discussion" href="http://test.com/facelessuser/pymdown-extensions/discussions/2" title="Test Discussion: facelessuser/pymdown-extensions #2">?2</a></p>'  # noqa: E501
+        )
+
+    def test_default_commit(self):
+        """Test default commit case."""
+
+        self.check_markdown(
+            '3f6b07a8eeaa9d606115758d90f55fec565d4e2a',
+            '<p><a class="magiclink magiclink-test magiclink-commit" href="http://test.com/facelessuser/pymdown-extensions/commit/3f6b07a8eeaa9d606115758d90f55fec565d4e2a" title="Test Commit: facelessuser/pymdown-extensions@3f6b07a">3f6b07a</a></p>'  # noqa: E501
+        )
+
+    def test_default_compare(self):
+        """Test default compare case."""
+
+        self.check_markdown(
+            'e2ed7e0b3973f3f9eb7a26b8ef7ae514eebfe0d2...90b6fb8711e75732f987982cc024e9bb0111beac',
+            '<p><a class="magiclink magiclink-test magiclink-compare" href="http://test.com/facelessuser/pymdown-extensions/compare/e2ed7e0b3973f3f9eb7a26b8ef7ae514eebfe0d2...90b6fb8711e75732f987982cc024e9bb0111beac" title="Test Compare: facelessuser/pymdown-extensions@e2ed7e0...90b6fb8">e2ed7e0...90b6fb8</a></p>'  # noqa: E501
+        )
+
+    def test_user_link(self):
+        """Test user link."""
+
+        self.check_markdown(
+            'http://test.com/facelessuser',
+            '<p><a class="magiclink magiclink-test magiclink-mention" href="http://test.com/facelessuser" title="Test User: facelessuser">@facelessuser</a></p>'  # noqa: E501
+        )
+
+    def test_repo_link(self):
+        """Test repository link."""
+
+        self.check_markdown(
+            'http://test.com/facelessuser/pymdown-extensions',
+            '<p><a class="magiclink magiclink-test magiclink-repository" href="http://test.com/facelessuser/pymdown-extensions" title="Test Repository: facelessuser/pymdown-extensions">facelessuser/pymdown-extensions</a></p>'  # noqa: E501
+        )
+
+    def test_issue_link(self):
+        """Test issue link."""
+
+        self.check_markdown(
+            'http://test.com/facelessuser/pymdown-extensions/issues/2',
+            '<p><a class="magiclink magiclink-test magiclink-issue" href="http://test.com/facelessuser/pymdown-extensions/issues/2" title="Test Issue: facelessuser/pymdown-extensions #2">#2</a></p>'  # noqa: E501
+        )
+
+    def test_pull_link(self):
+        """Test issue link."""
+
+        self.check_markdown(
+            'http://test.com/facelessuser/pymdown-extensions/pull/2',
+            '<p><a class="magiclink magiclink-test magiclink-pull" href="http://test.com/facelessuser/pymdown-extensions/pull/2" title="Test Pull Request: facelessuser/pymdown-extensions #2">!2</a></p>'  # noqa: E501
+        )
+
+    def test_discussion_link(self):
+        """Test discussion link."""
+
+        self.check_markdown(
+            'http://test.com/facelessuser/pymdown-extensions/discussions/2',
+            '<p><a class="magiclink magiclink-test magiclink-discussion" href="http://test.com/facelessuser/pymdown-extensions/discussions/2" title="Test Discussion: facelessuser/pymdown-extensions #2">?2</a></p>'  # noqa: E501
+        )
+
+    def test_commit_link(self):
+        """Test commit link."""
+
+        self.check_markdown(
+            'http://test.com/facelessuser/pymdown-extensions/commit/3f6b07a8eeaa9d606115758d90f55fec565d4e2a',
+            '<p><a class="magiclink magiclink-test magiclink-commit" href="http://test.com/facelessuser/pymdown-extensions/commit/3f6b07a8eeaa9d606115758d90f55fec565d4e2a" title="Test Commit: facelessuser/pymdown-extensions@3f6b07a">3f6b07a</a></p>'  # noqa: E501
+        )
+
+    def test_compare_link(self):
+        """Test compare link."""
+
+        self.check_markdown(
+            'http://test.com/facelessuser/pymdown-extensions/compare/e2ed7e0b3973f3f9eb7a26b8ef7ae514eebfe0d2...90b6fb8711e75732f987982cc024e9bb0111beac',
+            '<p><a class="magiclink magiclink-test magiclink-compare" href="http://test.com/facelessuser/pymdown-extensions/compare/e2ed7e0b3973f3f9eb7a26b8ef7ae514eebfe0d2...90b6fb8711e75732f987982cc024e9bb0111beac" title="Test Compare: facelessuser/pymdown-extensions@e2ed7e0...90b6fb8">e2ed7e0...90b6fb8</a></p>'  # noqa: E501
+        )
+
+    def test_external_user(self):
+        """Test external user in custom repo."""
+
+        self.check_markdown(
+            '@github:facelessuser',
+            '<p><a class="magiclink magiclink-github magiclink-mention" href="https://github.com/facelessuser" title="GitHub User: facelessuser">@facelessuser</a></p>'  # noqa: E501
+        )
+
+        self.check_markdown(
+            '@test:facelessuser',
+            '<p><a class="magiclink magiclink-test magiclink-mention" href="http://test.com/facelessuser" title="Test User: facelessuser">@facelessuser</a></p>'  # noqa: E501
+        )
+
+    def test_bad_name(self):
+        """Test bad name."""
+
+        extension = [
+            'pymdownx.magiclink',
+            'pymdownx.saneheaders'
+        ]
+
+        extension_configs = {
+            'pymdownx.magiclink': {
+                'repo_url_shorthand': True,
+                'repo_url_shortener': True,
+                'user': 'facelessuser',
+                'repo': 'pymdown-extensions',
+                'provider': 'bad-name',
+                'custom': {
+                    'bad-name': {
+                        'host': 'http://bad.com',
+                        'label': 'Bad',
+                        'type': 'github'
+                    }
+                }
+            }
+        }
+
+        with self.assertRaises(ValueError):
+            markdown.markdown('', extensions=extension, extension_configs=extension_configs)