1311 lines
47 KiB
Python
1311 lines
47 KiB
Python
|
"""
|
||
|
Magic Link.
|
||
|
|
||
|
pymdownx.magiclink
|
||
|
An extension for Python Markdown.
|
||
|
Find HTML, FTP links, and email address and turn them to actual links
|
||
|
|
||
|
MIT license.
|
||
|
|
||
|
Copyright (c) 2014 - 2017 Isaac Muse <isaacmuse@gmail.com>
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||
|
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions
|
||
|
of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||
|
DEALINGS IN THE SOFTWARE.
|
||
|
"""
|
||
|
from markdown import Extension
|
||
|
from markdown.treeprocessors import Treeprocessor
|
||
|
from markdown import util as md_util
|
||
|
import xml.etree.ElementTree as etree
|
||
|
from . import util
|
||
|
import re
|
||
|
from markdown.inlinepatterns import LinkInlineProcessor, InlineProcessor
|
||
|
|
||
|
MAGIC_LINK = 1
|
||
|
MAGIC_AUTO_LINK = 2
|
||
|
|
||
|
DEFAULT_EXCLUDES = {
|
||
|
"bitbucket": ['dashboard', 'account', 'plans', 'support', 'repo'],
|
||
|
"github": ['marketeplace', 'notifications', 'issues', 'pull', 'sponsors', 'settings', 'support'],
|
||
|
"gitlab": ['dashboard', '-', 'explore', 'help', 'projects'],
|
||
|
"twitter": ['i', 'messages', 'bookmarks', 'home']
|
||
|
}
|
||
|
|
||
|
# Bare link/email detection
|
||
|
RE_MAIL = r'''(?xi)
|
||
|
(?P<mail>
|
||
|
(?<![-/\+@a-z\d_])(?:[-+a-z\d_]([-a-z\d_+]|\.(?!\.))*) # Local part
|
||
|
(?<!\.)@(?:[-a-z\d_]+\.) # @domain part start
|
||
|
(?:(?:[-a-z\d_]|(?<!\.)\.(?!\.))*)[a-z]\b # @domain.end (allow multiple dot names)
|
||
|
(?![-@]) # Don't allow last char to be followed by these
|
||
|
)
|
||
|
'''
|
||
|
|
||
|
RE_LINK = r'''(?xi)
|
||
|
(?P<link>
|
||
|
(?:(?<=\b)|(?<=_))(?:
|
||
|
(?:ht|f)tps?://[^_\W][-\w]*(?:\.[-\w.]+)*| # (http|ftp)://
|
||
|
(?P<www>w{3}\.)[^_\W][-\w]*(?:\.[-\w.]+)* # www.
|
||
|
)
|
||
|
/?[-\w.?,!'(){}\[\]/+&@%$#=:"|~;]* # url path, fragments, and query stuff
|
||
|
(?:[^_\W]|[-/#@$+=]) # allowed end chars
|
||
|
)
|
||
|
'''
|
||
|
|
||
|
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])?'
|
||
|
RE_GITLAB_USER = r'[\.a-zA-Z\d_](?:[-a-zA-Z\d_\.]{0,37}[-a-zA-Z\d_])?'
|
||
|
RE_BITBUCKET_USER = r'[-a-zA-Z\d_]{1,39}'
|
||
|
|
||
|
# External mention patterns
|
||
|
RE_ALL_EXT_MENTIONS = r'''(?x)
|
||
|
(?P<mention>
|
||
|
(?<![a-zA-Z])@
|
||
|
(?:{})
|
||
|
)\b
|
||
|
'''
|
||
|
|
||
|
# 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 fr'{name}:{RE_GITHUB_USER}'
|
||
|
elif provider_type == 'gitlab':
|
||
|
return fr'{name}:{RE_GITLAB_USER}'
|
||
|
elif provider_type == 'bitbucket':
|
||
|
return fr'{name}:{RE_BITBUCKET_USER}'
|
||
|
|
||
|
RE_TWITTER_EXT_MENTIONS = fr'twitter:{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>
|
||
|
(?<![a-zA-Z])
|
||
|
@(?:{})
|
||
|
)\b
|
||
|
/(?P<mention_repo>[-._a-zA-Z\d]{{0,99}}[a-zA-Z\d])\b
|
||
|
'''
|
||
|
|
||
|
# Internal repo mention patterns
|
||
|
RE_GIT_INT_REPO_MENTIONS = r'''(?x)
|
||
|
(?P<mention>(?<![a-zA-Z])@{})\b
|
||
|
/(?P<mention_repo>[-._a-zA-Z\d]{{0,99}}[a-zA-Z\d])\b
|
||
|
'''
|
||
|
|
||
|
# External reference patterns (issue, pull request, commit, compare)
|
||
|
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
|
||
|
'''
|
||
|
|
||
|
# Internal reference patterns (issue, pull request, commit, compare)
|
||
|
RE_GIT_INT_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
|
||
|
'''
|
||
|
|
||
|
# Internal reference patterns for default user and repository (issue, pull request, commit, compare)
|
||
|
RE_GIT_INT_MICRO_REFS = r'''(?x)
|
||
|
(?P<all>
|
||
|
(?:(?<![a-zA-Z])(?P<issue>(?:\#|!|\?)[1-9][0-9]*)|(?P<commit>(?<![@/])\b[a-f\d]{40})(?:\.{3}(?P<diff>[a-f\d]{40}))?)
|
||
|
)\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)^(?:{}|{}|{})/?$'''.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)^(?:{}|{}|{})/?$'''.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(
|
||
|
r'''(?xi)
|
||
|
^(?:
|
||
|
(?P<twitter>(?P<twitter_base>https://(?:w{{3}}\.)?twitter\.com/(?P<twitter_user>{})))
|
||
|
)/?$
|
||
|
'''.format(RE_TWITTER_USER)
|
||
|
)
|
||
|
|
||
|
# Provider specific info (links, names, specific patterns, etc.)
|
||
|
SOCIAL_PROVIDERS = {'twitter'}
|
||
|
|
||
|
# Templates for providers
|
||
|
PROVIDER_TEMPLATES = {
|
||
|
"gitlab": {
|
||
|
"provider": "GitLab",
|
||
|
"type": "gitlab",
|
||
|
"url": "{}",
|
||
|
"user_pattern": RE_GITLAB_USER,
|
||
|
"issue": "{}/{{}}/{{}}/-/issues/{{}}",
|
||
|
"pull": "{}/{{}}/{{}}/-/merge_requests/{{}}",
|
||
|
"commit": "{}/{{}}/{{}}/-/commit/{{}}",
|
||
|
"compare": "{}/{{}}/{{}}/-/compare/{{}}...{{}}",
|
||
|
"hash_size": 8
|
||
|
},
|
||
|
"bitbucket": {
|
||
|
"provider": "Bitbucket",
|
||
|
"type": "bitbucket",
|
||
|
"url": "{}",
|
||
|
"user_pattern": RE_BITBUCKET_USER,
|
||
|
"issue": "{}/{{}}/{{}}/issues/{{}}",
|
||
|
"pull": "{}/{{}}/{{}}/pull-requests/{{}}",
|
||
|
"commit": "{}/{{}}/{{}}/commits/commit/{{}}",
|
||
|
"compare": "{}/{{}}/{{}}/branches/commits/{{}}..{{}}#diff",
|
||
|
"hash_size": 7
|
||
|
},
|
||
|
"github": {
|
||
|
"provider": "GitHub",
|
||
|
"type": "github",
|
||
|
"url": "{}",
|
||
|
"user_pattern": RE_GITHUB_USER,
|
||
|
"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, provider_info):
|
||
|
"""Initialize."""
|
||
|
|
||
|
self.user = user
|
||
|
self.repo = repo
|
||
|
self.labels = labels
|
||
|
self.normalize = normalize
|
||
|
self.provider_info = provider_info
|
||
|
self.provider = provider if provider in self.provider_info else ''
|
||
|
InlineProcessor.__init__(self, pattern, md)
|
||
|
|
||
|
|
||
|
class _MagiclinkReferencePattern(_MagiclinkShorthandPattern):
|
||
|
"""Convert #1, repo#1, user/repo#1, !1, repo!1, user/repo!1, hash, repo@hash, or user/repo@hash to links."""
|
||
|
|
||
|
def process_issues(self, el, provider, user, repo, issue):
|
||
|
"""Process issues."""
|
||
|
|
||
|
issue_type = issue[:1]
|
||
|
issue_value = issue[1:]
|
||
|
|
||
|
if issue_type == '#':
|
||
|
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 = 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 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
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
if self.my_repo:
|
||
|
el.text = md_util.AtomicString(f'{icon}{issue_value}')
|
||
|
elif self.my_user:
|
||
|
el.text = md_util.AtomicString(f'{repo}{icon}{issue_value}')
|
||
|
else:
|
||
|
el.text = md_util.AtomicString(f'{user}/{repo}{icon}{issue_value}')
|
||
|
|
||
|
el.set('href', issue_link.format(user, repo, issue_value))
|
||
|
el.set('class', f'magiclink magiclink-{provider} {class_name}')
|
||
|
el.set(
|
||
|
'title',
|
||
|
'{} {}: {}/{} #{}'.format(
|
||
|
self.provider_info[provider]['provider'],
|
||
|
issue_label,
|
||
|
user,
|
||
|
repo,
|
||
|
issue_value
|
||
|
)
|
||
|
)
|
||
|
return True
|
||
|
|
||
|
def process_commit(self, el, provider, user, repo, commit):
|
||
|
"""Process commit."""
|
||
|
|
||
|
hash_ref = commit[0:self.provider_info[provider]['hash_size']]
|
||
|
if self.my_repo:
|
||
|
text = hash_ref
|
||
|
elif self.my_user:
|
||
|
text = f'{repo}@{hash_ref}'
|
||
|
else:
|
||
|
text = f'{user}/{repo}@{hash_ref}'
|
||
|
|
||
|
el.set('href', self.provider_info[provider]['commit'].format(user, repo, commit))
|
||
|
el.text = md_util.AtomicString(text)
|
||
|
el.set('class', f'magiclink magiclink-{provider} magiclink-commit')
|
||
|
el.set(
|
||
|
'title',
|
||
|
'{} {}: {}/{}@{}'.format(
|
||
|
self.provider_info[provider]['provider'],
|
||
|
self.labels.get('commit', 'Commit'),
|
||
|
user,
|
||
|
repo,
|
||
|
hash_ref
|
||
|
)
|
||
|
)
|
||
|
|
||
|
def process_compare(self, el, provider, user, repo, commit1, commit2):
|
||
|
"""Process commit."""
|
||
|
|
||
|
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 = f'{hash_ref1}...{hash_ref2}'
|
||
|
elif self.my_user:
|
||
|
text = f'{repo}@{hash_ref1}...{hash_ref2}'
|
||
|
else:
|
||
|
text = f'{user}/{repo}@{hash_ref1}...{hash_ref2}'
|
||
|
|
||
|
el.set('href', self.provider_info[provider]['compare'].format(user, repo, commit1, commit2))
|
||
|
el.text = md_util.AtomicString(text)
|
||
|
el.set('class', f'magiclink magiclink-{provider} magiclink-compare')
|
||
|
el.set(
|
||
|
'title',
|
||
|
'{} {}: {}/{}@{}...{}'.format(
|
||
|
self.provider_info[provider]['provider'],
|
||
|
self.labels.get('compare', 'Compare'),
|
||
|
user,
|
||
|
repo,
|
||
|
hash_ref1,
|
||
|
hash_ref2
|
||
|
)
|
||
|
)
|
||
|
|
||
|
|
||
|
class MagicShortenerTreeprocessor(Treeprocessor):
|
||
|
"""Tree processor that finds repo issue and commit links and shortens them."""
|
||
|
|
||
|
# Repo link types
|
||
|
ISSUE = 0
|
||
|
PULL = 1
|
||
|
COMMIT = 2
|
||
|
DISCUSS = 3
|
||
|
DIFF = 4
|
||
|
REPO = 5
|
||
|
USER = 6
|
||
|
|
||
|
def __init__(
|
||
|
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",
|
||
|
"gitlab": "GitLab"
|
||
|
}
|
||
|
self.excludes = excludes
|
||
|
Treeprocessor.__init__(self, md)
|
||
|
|
||
|
def shorten_repo(self, link, class_name, label, user_repo):
|
||
|
"""Shorten repo link."""
|
||
|
|
||
|
text = user_repo
|
||
|
link.text = md_util.AtomicString(text)
|
||
|
|
||
|
if 'magiclink-repository' not in class_name:
|
||
|
class_name.append('magiclink-repository')
|
||
|
|
||
|
link.set(
|
||
|
'title',
|
||
|
"{} {}: {}".format(
|
||
|
label, self.repo_labels.get('repository', 'Repository'), user_repo
|
||
|
)
|
||
|
)
|
||
|
|
||
|
def shorten_user(self, link, class_name, label, user_repo):
|
||
|
"""Shorten user link."""
|
||
|
|
||
|
link.text = md_util.AtomicString(f'@{user_repo}')
|
||
|
|
||
|
if 'magiclink-mention' not in class_name:
|
||
|
class_name.append('magiclink-mention')
|
||
|
|
||
|
link.set(
|
||
|
'title',
|
||
|
"{} {}: {}".format(
|
||
|
label, self.repo_labels.get('metion', 'User'), user_repo
|
||
|
)
|
||
|
)
|
||
|
|
||
|
def shorten_diff(self, link, class_name, label, user_repo, value, hash_size):
|
||
|
"""Shorten diff/compare links."""
|
||
|
|
||
|
repo_label = self.repo_labels.get('compare', 'Compare')
|
||
|
if self.my_repo:
|
||
|
text = f'{value[0][0:hash_size]}...{value[1][0:hash_size]}'
|
||
|
elif self.my_user:
|
||
|
text = '{}@{}...{}'.format(user_repo.split('/')[1], value[0][0:hash_size], value[1][0:hash_size])
|
||
|
else:
|
||
|
text = f'{user_repo}@{value[0][0:hash_size]}...{value[1][0:hash_size]}'
|
||
|
link.text = md_util.AtomicString(text)
|
||
|
|
||
|
if 'magiclink-compare' not in class_name:
|
||
|
class_name.append('magiclink-compare')
|
||
|
|
||
|
link.set(
|
||
|
'title',
|
||
|
'{} {}: {}@{}...{}'.format(
|
||
|
label, repo_label, user_repo.rstrip('/'), value[0][0:hash_size], value[1][0:hash_size]
|
||
|
)
|
||
|
)
|
||
|
|
||
|
def shorten_commit(self, link, class_name, label, user_repo, value, hash_size):
|
||
|
"""Shorten commit link."""
|
||
|
|
||
|
# user/repo@hash
|
||
|
repo_label = self.repo_labels.get('commit', 'Commit')
|
||
|
if self.my_repo:
|
||
|
text = value[0:hash_size]
|
||
|
elif self.my_user:
|
||
|
text = '{}@{}'.format(user_repo.split('/')[1], value[0:hash_size])
|
||
|
else:
|
||
|
text = f'{user_repo}@{value[0:hash_size]}'
|
||
|
link.text = md_util.AtomicString(text)
|
||
|
|
||
|
if 'magiclink-commit' not in class_name:
|
||
|
class_name.append('magiclink-commit')
|
||
|
|
||
|
link.set(
|
||
|
'title',
|
||
|
'{} {}: {}@{}'.format(label, repo_label, user_repo.rstrip('/'), value[0:hash_size])
|
||
|
)
|
||
|
|
||
|
def shorten_issue(self, provider, link, class_name, label, user_repo, value, link_type):
|
||
|
"""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 = '#'
|
||
|
if 'magiclink-issue' not in class_name:
|
||
|
class_name.append('magiclink-issue')
|
||
|
elif link_type == self.PULL:
|
||
|
issue_type = self.repo_labels.get('pull', 'Pull Request')
|
||
|
icon = '#' if self.normalize else '!'
|
||
|
if 'magiclink-pull' not in class_name:
|
||
|
class_name.append('magiclink-pull')
|
||
|
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:
|
||
|
class_name.append('magiclink-discussion')
|
||
|
|
||
|
if self.my_repo:
|
||
|
link.text = md_util.AtomicString(f"{icon}{value}")
|
||
|
elif self.my_user:
|
||
|
link.text = md_util.AtomicString("{}{}{}".format(user_repo.split('/')[1], icon, value))
|
||
|
else:
|
||
|
link.text = md_util.AtomicString(f"{user_repo}{icon}{value}")
|
||
|
|
||
|
link.set('title', '{} {}: {} #{}'.format(label, issue_type, user_repo.rstrip('/'), value))
|
||
|
|
||
|
def shorten_issue_commit(self, link, provider, link_type, user_repo, value, hash_size):
|
||
|
"""Shorten URL."""
|
||
|
|
||
|
label = self.provider_info[provider]['provider']
|
||
|
prov_class = f'magiclink-{provider}'
|
||
|
class_attr = link.get('class', '')
|
||
|
class_name = class_attr.split(' ') if class_attr else []
|
||
|
|
||
|
if 'magiclink' not in class_name:
|
||
|
class_name.append('magiclink')
|
||
|
|
||
|
if prov_class not in class_name:
|
||
|
class_name.append(prov_class)
|
||
|
|
||
|
# Link specific shortening logic
|
||
|
if link_type is self.DIFF:
|
||
|
self.shorten_diff(link, class_name, label, user_repo, value, hash_size)
|
||
|
elif link_type is self.COMMIT:
|
||
|
self.shorten_commit(link, class_name, label, user_repo, value, hash_size)
|
||
|
else:
|
||
|
self.shorten_issue(provider, link, class_name, label, user_repo, value, link_type)
|
||
|
link.set('class', ' '.join(class_name))
|
||
|
|
||
|
def shorten_user_repo(self, link, provider, link_type, user_repo):
|
||
|
"""Shorten URL."""
|
||
|
|
||
|
label = self.provider_info[provider]['provider']
|
||
|
prov_class = f'magiclink-{provider}'
|
||
|
class_attr = link.get('class', '')
|
||
|
class_name = class_attr.split(' ') if class_attr else []
|
||
|
|
||
|
if 'magiclink' not in class_name:
|
||
|
class_name.append('magiclink')
|
||
|
|
||
|
if prov_class not in class_name:
|
||
|
class_name.append(prov_class)
|
||
|
|
||
|
# Link specific shortening logic
|
||
|
if link_type is self.REPO:
|
||
|
self.shorten_repo(link, class_name, label, user_repo)
|
||
|
else:
|
||
|
self.shorten_user(link, class_name, label, user_repo)
|
||
|
link.set('class', ' '.join(class_name))
|
||
|
|
||
|
def get_provider_type(self, match):
|
||
|
"""Get the provider and hash size."""
|
||
|
|
||
|
# Set provider specific variables
|
||
|
if match.group('github'):
|
||
|
provider = 'github'
|
||
|
elif match.group('bitbucket'):
|
||
|
provider = 'bitbucket'
|
||
|
elif match.group('gitlab'):
|
||
|
provider = 'gitlab'
|
||
|
return provider
|
||
|
|
||
|
def get_social_provider(self, match):
|
||
|
"""Get social provider."""
|
||
|
|
||
|
if match.group('twitter'):
|
||
|
provider = 'twitter'
|
||
|
return provider
|
||
|
|
||
|
def get_type(self, provider, match):
|
||
|
"""Get the link type."""
|
||
|
|
||
|
try:
|
||
|
# Gather info about link type
|
||
|
if match.group(provider + '_diff1') is not None:
|
||
|
value = (match.group(provider + '_diff1'), match.group(provider + '_diff2'))
|
||
|
link_type = self.DIFF
|
||
|
elif match.group(provider + '_commit') is not None:
|
||
|
value = match.group(provider + '_commit')
|
||
|
link_type = self.COMMIT
|
||
|
elif match.group(provider + '_pull') is not None:
|
||
|
value = match.group(provider + '_pull')
|
||
|
link_type = self.PULL
|
||
|
elif provider == "github" and match.group(provider + '_discuss') is not None:
|
||
|
value = match.group(provider + '_discuss')
|
||
|
link_type = self.DISCUSS
|
||
|
else:
|
||
|
value = match.group(provider + '_issue')
|
||
|
link_type = self.ISSUE
|
||
|
except IndexError:
|
||
|
# Gather info about link type
|
||
|
found = False
|
||
|
try:
|
||
|
if match.group(provider + '_repo') is not None:
|
||
|
value = None
|
||
|
link_type = self.REPO
|
||
|
found = True
|
||
|
except IndexError:
|
||
|
pass
|
||
|
if not found:
|
||
|
value = None
|
||
|
link_type = self.USER
|
||
|
return value, link_type
|
||
|
|
||
|
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_type + '_base') + '/' == self.base
|
||
|
|
||
|
def is_my_user(self, provider_type, match):
|
||
|
"""Check if link is from our specified user."""
|
||
|
|
||
|
return self.base_user and match.group(provider_type + '_base').startswith(self.base_user)
|
||
|
|
||
|
def excluded(self, provider_type, provider, match):
|
||
|
"""Check if user has been excluded."""
|
||
|
|
||
|
user = match.group(provider_type + '_user')
|
||
|
return user.lower() in self.excludes.get(provider, set())
|
||
|
|
||
|
def run(self, root):
|
||
|
"""Shorten popular git repository links."""
|
||
|
|
||
|
self.hide_protocol = self.config['hide_protocol']
|
||
|
|
||
|
links = root.iter('a')
|
||
|
for link in links:
|
||
|
has_child = len(list(link))
|
||
|
is_magic = link.attrib.get('magiclink')
|
||
|
href = link.attrib.get('href', '')
|
||
|
text = link.text
|
||
|
found = False
|
||
|
|
||
|
if is_magic:
|
||
|
del link.attrib['magiclink']
|
||
|
|
||
|
# We want a normal link. No sub-elements embedded in it, just a normal string.
|
||
|
if has_child or not text: # pragma: no cover
|
||
|
continue
|
||
|
|
||
|
# Make sure the text matches the `href`. If needed, add back protocol to be sure.
|
||
|
# Not all links will pass through MagicLink, so we try both with and without protocol.
|
||
|
if (text == href or (is_magic and self.hide_protocol and ('https://' + text) == href)):
|
||
|
if self.repo_shortner:
|
||
|
m = RE_REPO_LINK.match(href)
|
||
|
if 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_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 and self.repo_shortner:
|
||
|
m = RE_USER_REPO_LINK.match(href)
|
||
|
if 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_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.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:
|
||
|
provider = self.get_social_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)
|
||
|
|
||
|
if not self.excluded(provider, provider, m):
|
||
|
# All right, everything set, let's shorten.
|
||
|
self.shorten_user_repo(
|
||
|
link,
|
||
|
provider,
|
||
|
link_type,
|
||
|
m.group(provider + '_user')
|
||
|
)
|
||
|
return root
|
||
|
|
||
|
|
||
|
class MagiclinkPattern(LinkInlineProcessor):
|
||
|
"""Convert html, ftp links to clickable links."""
|
||
|
|
||
|
ANCESTOR_EXCLUDES = ('a',)
|
||
|
|
||
|
def handleMatch(self, m, data):
|
||
|
"""Handle URL matches."""
|
||
|
|
||
|
el = etree.Element("a")
|
||
|
el.text = md_util.AtomicString(m.group('link'))
|
||
|
if m.group("www"):
|
||
|
href = "http://{}".format(m.group('link'))
|
||
|
else:
|
||
|
href = m.group('link')
|
||
|
if self.config['hide_protocol']:
|
||
|
el.text = md_util.AtomicString(el.text[el.text.find("://") + 3:])
|
||
|
el.set("href", self.unescape(href.strip()))
|
||
|
|
||
|
if self.config.get('repo_url_shortener', False):
|
||
|
el.set('magiclink', str(MAGIC_LINK))
|
||
|
|
||
|
return el, m.start(0), m.end(0)
|
||
|
|
||
|
|
||
|
class MagiclinkAutoPattern(InlineProcessor):
|
||
|
"""Return a link Element given an auto link `<http://example/com>`."""
|
||
|
|
||
|
def handleMatch(self, m, data):
|
||
|
"""Return link optionally without protocol."""
|
||
|
|
||
|
el = etree.Element("a")
|
||
|
el.set('href', self.unescape(m.group(1)))
|
||
|
el.text = md_util.AtomicString(m.group(1))
|
||
|
if self.config['hide_protocol']:
|
||
|
el.text = md_util.AtomicString(el.text[el.text.find("://") + 3:])
|
||
|
|
||
|
if self.config.get('repo_url_shortener', False):
|
||
|
el.set('magiclink', str(MAGIC_AUTO_LINK))
|
||
|
|
||
|
return el, m.start(0), m.end(0)
|
||
|
|
||
|
|
||
|
class MagiclinkMailPattern(InlineProcessor):
|
||
|
"""Convert emails to clickable email links."""
|
||
|
|
||
|
ANCESTOR_EXCLUDES = ('a',)
|
||
|
|
||
|
def email_encode(self, code):
|
||
|
"""Return entity definition by code, or the code if not defined."""
|
||
|
return f"{md_util.AMP_SUBSTITUTE}#{code:d};"
|
||
|
|
||
|
def handleMatch(self, m, data):
|
||
|
"""Handle email link patterns."""
|
||
|
|
||
|
el = etree.Element("a")
|
||
|
email = self.unescape(m.group('mail'))
|
||
|
href = f"mailto:{email}"
|
||
|
el.text = md_util.AtomicString(''.join([self.email_encode(ord(c)) for c in email]))
|
||
|
el.set("href", ''.join([md_util.AMP_SUBSTITUTE + f'#{ord(c):d};' for c in href]))
|
||
|
return el, m.start(0), m.end(0)
|
||
|
|
||
|
|
||
|
class MagiclinkMentionPattern(_MagiclinkShorthandPattern):
|
||
|
"""Convert @mention to links."""
|
||
|
|
||
|
ANCESTOR_EXCLUDES = ('a',)
|
||
|
|
||
|
def handleMatch(self, m, data):
|
||
|
"""Handle email link patterns."""
|
||
|
|
||
|
text = m.group('mention')[1:]
|
||
|
parts = text.split(':')
|
||
|
if len(parts) > 1:
|
||
|
provider = parts[0]
|
||
|
mention = parts[1]
|
||
|
else:
|
||
|
provider = self.provider
|
||
|
mention = parts[0]
|
||
|
|
||
|
el = etree.Element("a")
|
||
|
el.set('href', '{}/{}'.format(self.provider_info[provider]['url'], mention))
|
||
|
el.set(
|
||
|
'title',
|
||
|
"{} {}: {}".format(self.provider_info[provider]['provider'], self.labels.get('mention', "User"), mention)
|
||
|
)
|
||
|
el.set('class', f'magiclink magiclink-{provider} magiclink-mention')
|
||
|
el.text = md_util.AtomicString(f'@{mention}')
|
||
|
|
||
|
return el, m.start(0), m.end(0)
|
||
|
|
||
|
|
||
|
class MagiclinkRepositoryPattern(_MagiclinkShorthandPattern):
|
||
|
"""Convert @user/repo to links."""
|
||
|
|
||
|
ANCESTOR_EXCLUDES = ('a',)
|
||
|
|
||
|
def handleMatch(self, m, data):
|
||
|
"""Handle email link patterns."""
|
||
|
|
||
|
text = m.group('mention')[1:]
|
||
|
parts = text.split(':')
|
||
|
if len(parts) > 1:
|
||
|
provider = parts[0]
|
||
|
user = parts[1]
|
||
|
else:
|
||
|
provider = self.provider
|
||
|
user = parts[0]
|
||
|
repo = m.group('mention_repo')
|
||
|
|
||
|
el = etree.Element("a")
|
||
|
el.set('href', '{}/{}/{}'.format(self.provider_info[provider]['url'], user, repo))
|
||
|
el.set(
|
||
|
'title',
|
||
|
"{} {}: {}/{}".format(
|
||
|
self.provider_info[provider]['provider'], self.labels.get('repository', 'Repository'), user, repo
|
||
|
)
|
||
|
)
|
||
|
el.set('class', f'magiclink magiclink-{provider} magiclink-repository')
|
||
|
el.text = md_util.AtomicString(f'{user}/{repo}')
|
||
|
return el, m.start(0), m.end(0)
|
||
|
|
||
|
|
||
|
class MagiclinkExternalRefsPattern(_MagiclinkReferencePattern):
|
||
|
"""Convert repo#1, user/repo#1, repo!1, user/repo!1, repo@hash, or user/repo@hash to links."""
|
||
|
|
||
|
ANCESTOR_EXCLUDES = ('a',)
|
||
|
|
||
|
def handleMatch(self, m, data):
|
||
|
"""Handle email link patterns."""
|
||
|
|
||
|
is_commit = m.group('commit')
|
||
|
is_diff = m.group('diff')
|
||
|
value = m.group('commit')[1:] if is_commit else m.group('issue')
|
||
|
value2 = m.group('diff') if is_diff else None
|
||
|
repo = m.group('repo')
|
||
|
user = m.group('user')
|
||
|
|
||
|
if not user:
|
||
|
user = self.user
|
||
|
|
||
|
parts = user.split(':')
|
||
|
if len(parts) > 1:
|
||
|
provider = parts[0]
|
||
|
user = parts[1]
|
||
|
else:
|
||
|
provider = self.provider
|
||
|
|
||
|
# If there is no valid user or provider, reject
|
||
|
if not user:
|
||
|
return None, None, None
|
||
|
|
||
|
self.my_user = user == self.user and provider == self.provider
|
||
|
self.my_repo = self.my_user and repo == self.repo
|
||
|
|
||
|
el = etree.Element("a")
|
||
|
if is_diff:
|
||
|
self.process_compare(el, provider, user, repo, value, value2)
|
||
|
elif is_commit:
|
||
|
self.process_commit(el, provider, user, repo, value)
|
||
|
else:
|
||
|
if not self.process_issues(el, provider, user, repo, value):
|
||
|
return m.group(0), m.start(0), m.end(0)
|
||
|
return el, m.start(0), m.end(0)
|
||
|
|
||
|
|
||
|
class MagiclinkInternalRefsPattern(_MagiclinkReferencePattern):
|
||
|
"""Convert #1, !1, and commit_hash."""
|
||
|
|
||
|
ANCESTOR_EXCLUDES = ('a',)
|
||
|
|
||
|
def handleMatch(self, m, data):
|
||
|
"""Handle email link patterns."""
|
||
|
|
||
|
# We don't have a valid provider, user, and repo, reject
|
||
|
if not self.user or not self.repo:
|
||
|
return None, None, None
|
||
|
|
||
|
is_commit = m.group('commit')
|
||
|
is_diff = m.group('diff')
|
||
|
value = m.group('commit') if is_commit else m.group('issue')
|
||
|
value2 = m.group('diff') if is_diff else None
|
||
|
|
||
|
repo = self.repo
|
||
|
user = self.user
|
||
|
provider = self.provider
|
||
|
self.my_repo = True
|
||
|
self.my_user = True
|
||
|
|
||
|
el = etree.Element("a")
|
||
|
if is_diff:
|
||
|
self.process_compare(el, provider, user, repo, value, value2)
|
||
|
elif is_commit:
|
||
|
self.process_commit(el, provider, user, repo, value)
|
||
|
else:
|
||
|
if not self.process_issues(el, provider, user, repo, value):
|
||
|
return m.group(0), m.start(0), m.end(0)
|
||
|
return el, m.start(0), m.end(0)
|
||
|
|
||
|
|
||
|
class MagiclinkExtension(Extension):
|
||
|
"""Add auto link and link transformation extensions to Markdown class."""
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
"""Initialize."""
|
||
|
|
||
|
self.config = {
|
||
|
'hide_protocol': [
|
||
|
False,
|
||
|
"If 'True', links are displayed without the initial ftp://, http:// or https://"
|
||
|
"- Default: False"
|
||
|
],
|
||
|
'repo_url_shortener': [
|
||
|
False,
|
||
|
"If 'True' repo commit and issue links are shortened - Default: False"
|
||
|
],
|
||
|
'social_url_shortener': [
|
||
|
False,
|
||
|
"If 'True' social links are shortened - Default: False"
|
||
|
],
|
||
|
'shortener_user_exclude': [
|
||
|
{
|
||
|
"bitbucket": ['dashboard', 'account', 'plans', 'support', 'repo'],
|
||
|
"github": ['marketeplace', 'notifications', 'issues', 'pull', 'sponsors', 'settings', 'support'],
|
||
|
"gitlab": ['dashboard', '-', 'explore', 'help', 'projects'],
|
||
|
"twitter": ['i', 'messages', 'bookmarks', 'home']
|
||
|
},
|
||
|
"A list of user names to exclude from URL shortening."
|
||
|
],
|
||
|
'repo_url_shorthand': [
|
||
|
False,
|
||
|
"If 'True' repo shorthand syntax is converted to links - Default: False"
|
||
|
],
|
||
|
'social_url_shorthand': [
|
||
|
False,
|
||
|
"If 'True' social shorthand syntax is converted to links - Default: False"
|
||
|
],
|
||
|
'provider': [
|
||
|
'github',
|
||
|
'The base provider to use (github, gitlab, bitbucket, twitter) - Default: "github"'
|
||
|
],
|
||
|
'labels': [
|
||
|
{},
|
||
|
"Title labels - Default: {}"
|
||
|
],
|
||
|
'normalize_issue_symbols': [
|
||
|
False,
|
||
|
'Normalize issue, pull, and discussions symbols all to use # - Default: False'
|
||
|
],
|
||
|
'user': [
|
||
|
'',
|
||
|
'The base user name to use - Default: ""'
|
||
|
],
|
||
|
'repo': [
|
||
|
'',
|
||
|
'The base repo to use - Default: ""'
|
||
|
],
|
||
|
'custom': [
|
||
|
{},
|
||
|
"Custom repositories hosts - Default {}"
|
||
|
]
|
||
|
}
|
||
|
super().__init__(*args, **kwargs)
|
||
|
|
||
|
def setup_autolinks(self, md, config):
|
||
|
"""Setup auto links."""
|
||
|
|
||
|
# Setup general link patterns
|
||
|
auto_link_pattern = MagiclinkAutoPattern(RE_AUTOLINK, md)
|
||
|
auto_link_pattern.config = config
|
||
|
md.inlinePatterns.register(auto_link_pattern, "autolink", 120)
|
||
|
|
||
|
link_pattern = MagiclinkPattern(RE_LINK, md)
|
||
|
link_pattern.config = config
|
||
|
md.inlinePatterns.register(link_pattern, "magic-link", 85)
|
||
|
|
||
|
md.inlinePatterns.register(MagiclinkMailPattern(RE_MAIL, md), "magic-mail", 84.9)
|
||
|
|
||
|
def setup_shorthand(self, md):
|
||
|
"""Setup shorthand."""
|
||
|
|
||
|
# Setup URL shortener
|
||
|
escape_chars = ['@']
|
||
|
util.escape_chars(md, escape_chars)
|
||
|
|
||
|
# Repository shorthand
|
||
|
if self.git_short:
|
||
|
git_ext_repo = MagiclinkRepositoryPattern(
|
||
|
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(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(self.ext_mentions))
|
||
|
git_mention = MagiclinkMentionPattern(
|
||
|
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(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(
|
||
|
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(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,
|
||
|
self.provider_info
|
||
|
)
|
||
|
md.inlinePatterns.register(git_int_micro_refs, "magic-int-micro-refs", 79.3)
|
||
|
|
||
|
def setup_shortener(
|
||
|
self,
|
||
|
md,
|
||
|
config
|
||
|
):
|
||
|
"""Setup shortener."""
|
||
|
|
||
|
shortener = MagicShortenerTreeprocessor(
|
||
|
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)
|
||
|
|
||
|
def get_base_urls(self, config):
|
||
|
"""Get base URLs."""
|
||
|
|
||
|
base_url = ''
|
||
|
base_user_url = ''
|
||
|
|
||
|
if self.is_social:
|
||
|
return base_url, base_user_url
|
||
|
|
||
|
if self.user and self.repo:
|
||
|
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
|
||
|
|
||
|
def extendMarkdown(self, md):
|
||
|
"""Add support for turning html links and emails to link tags."""
|
||
|
|
||
|
config = self.getConfigs()
|
||
|
|
||
|
# Setup repo variables
|
||
|
self.user = config.get('user', '')
|
||
|
self.repo = config.get('repo', '')
|
||
|
self.provider = config.get('provider', 'github')
|
||
|
self.labels = config.get('labels', {})
|
||
|
self.normalize = config.get('normalize_issue_symbols', False)
|
||
|
self.is_social = self.provider in SOCIAL_PROVIDERS
|
||
|
self.git_short = config.get('repo_url_shorthand', False)
|
||
|
self.social_short = config.get('social_url_shorthand', False)
|
||
|
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(
|
||
|
f"Name '{custom}' not allowed, provider name must contain only letters and numbers"
|
||
|
)
|
||
|
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 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 self.provider_info:
|
||
|
self.provider = 'github'
|
||
|
|
||
|
self.setup_autolinks(md, config)
|
||
|
|
||
|
if self.git_short or self.social_short:
|
||
|
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:
|
||
|
self.base_url, self.base_user_url = self.get_base_urls(config)
|
||
|
self.setup_shortener(md, config)
|
||
|
|
||
|
|
||
|
def makeExtension(*args, **kwargs):
|
||
|
"""Return extension."""
|
||
|
|
||
|
return MagiclinkExtension(*args, **kwargs)
|