| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025 |
- """
- 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?://[^<>]*)>'
- # 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])@
- (?:%s)
- )\b
- '''
- RE_TWITTER_EXT_MENTIONS = r'twitter:%s' % RE_TWITTER_USER
- RE_GITHUB_EXT_MENTIONS = r'github:%s' % RE_GITHUB_USER
- RE_GITLAB_EXT_MENTIONS = r'gitlab:%s' % RE_GITLAB_USER
- RE_BITBUCKET_EXT_MENTIONS = r'bitbucket:%s' % RE_BITBUCKET_USER
- # Internal mention patterns
- RE_INT_MENTIONS = r'(?P<mention>(?<![a-zA-Z])@%s)\b'
- # External repo mention patterns
- RE_GIT_EXT_REPO_MENTIONS = r'''(?x)
- (?P<mention>
- (?<![a-zA-Z])
- @(?:%s)
- )\b
- /(?P<mention_repo>[-._a-zA-Z\d]{0,99}[a-zA-Z\d])\b
- ''' % '|'.join([RE_GITHUB_EXT_MENTIONS, RE_GITLAB_EXT_MENTIONS, RE_BITBUCKET_EXT_MENTIONS])
- # Internal repo mention patterns
- RE_GIT_INT_REPO_MENTIONS = r'''(?x)
- (?P<mention>(?<![a-zA-Z])@%s)\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%s)/)
- (?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
- ''' % '|'.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)
- (?P<all>(?<![@/])(?:(?P<user>\b%s)/)?
- (?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
- '''
- # 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+)/?|
- commit/(?P<github_commit>[\da-f]{{40}})/?|
- compare/(?P<github_diff1>[\da-f]{{40}})\.{{3}}
- (?P<github_diff2>[\da-f]{{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]{{40}})/?|
- branches/commits/(?P<bitbucket_diff1>[\da-f]{{40}})
- (?:\.{{2}}|%0d)(?P<bitbucket_diff2>[\da-f]{{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]{{40}})/?|
- compare/(?P<gitlab_diff1>[\da-f]{{40}})\.{{3}}
- (?P<gitlab_diff2>[\da-f]{{40}})))
- )/?$
- '''.format(RE_GITHUB_USER, RE_BITBUCKET_USER, RE_GITLAB_USER)
- )
- # 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)
- )
- 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'}
- PROVIDER_INFO = {
- "twitter": {
- "provider": "Twitter",
- "url": "https://twitter.com",
- "user_pattern": RE_TWITTER_USER
- },
- "gitlab": {
- "provider": "GitLab",
- "url": "https://gitlab.com",
- "user_pattern": RE_GITLAB_USER,
- "issue": "https://gitlab.com/%s/%s/issues/%s",
- "pull": "https://gitlab.com/%s/%s/merge_requests/%s",
- "commit": "https://gitlab.com/%s/%s/commit/%s",
- "compare": "https://gitlab.com/%s/%s/compare/%s...%s",
- "hash_size": 8
- },
- "bitbucket": {
- "provider": "Bitbucket",
- "url": "https://bitbucket.org",
- "user_pattern": RE_BITBUCKET_USER,
- "issue": "https://bitbucket.org/%s/%s/issues/%s",
- "pull": "https://bitbucket.org/%s/%s/pull-requests/%s",
- "commit": "https://bitbucket.org/%s/%s/commits/commit/%s",
- "compare": "https://bitbucket.org/%s/%s/branches/commits/%s..%s#diff",
- "hash_size": 7
- },
- "github": {
- "provider": "GitHub",
- "url": "https://github.com",
- "user_pattern": RE_GITHUB_USER,
- "issue": "https://github.com/%s/%s/issues/%s",
- "pull": "https://github.com/%s/%s/pull/%s",
- "commit": "https://github.com/%s/%s/commit/%s",
- "compare": "https://github.com/%s/%s/compare/%s...%s",
- "hash_size": 7
- }
- }
- class _MagiclinkShorthandPattern(InlineProcessor):
- """Base shorthand link class."""
- def __init__(self, pattern, md, user, repo, provider, labels):
- """Initialize."""
- self.user = user
- self.repo = repo
- self.labels = labels
- self.provider = provider if provider in 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 = PROVIDER_INFO[provider]['issue']
- issue_label = self.labels.get('issue', 'Issue')
- class_name = 'magiclink-issue'
- else:
- issue_link = PROVIDER_INFO[provider]['pull']
- issue_label = self.labels.get('pull', 'Pull Request')
- class_name = 'magiclink-pull'
- if self.my_repo:
- text = '%s%s' % (issue_type, issue_value)
- elif self.my_user:
- text = '%s%s%s' % (repo, issue_type, issue_value)
- else:
- text = '%s/%s%s%s' % (user, repo, issue_type, issue_value)
- el.set('href', issue_link % (user, repo, issue_value))
- el.text = md_util.AtomicString(text)
- el.set('class', 'magiclink magiclink-%s %s' % (provider, class_name))
- el.set(
- 'title',
- '%s %s: %s/%s%s%s' % (
- PROVIDER_INFO[provider]['provider'],
- issue_label,
- user,
- repo,
- issue_type,
- issue_value
- )
- )
- def process_commit(self, el, provider, user, repo, commit):
- """Process commit."""
- hash_ref = commit[0:PROVIDER_INFO[provider]['hash_size']]
- if self.my_repo:
- text = hash_ref
- elif self.my_user:
- text = '%s@%s' % (repo, hash_ref)
- else:
- text = '%s/%s@%s' % (user, repo, hash_ref)
- el.set('href', PROVIDER_INFO[provider]['commit'] % (user, repo, commit))
- el.text = md_util.AtomicString(text)
- el.set('class', 'magiclink magiclink-%s magiclink-commit' % provider)
- el.set(
- 'title',
- '%s %s: %s/%s@%s' % (
- 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:PROVIDER_INFO[provider]['hash_size']]
- hash_ref2 = commit2[0:PROVIDER_INFO[provider]['hash_size']]
- if self.my_repo:
- text = '%s...%s' % (hash_ref1, hash_ref2)
- elif self.my_user:
- text = '%s@%s...%s' % (repo, hash_ref1, hash_ref2)
- else:
- text = '%s/%s@%s...%s' % (user, repo, hash_ref1, hash_ref2)
- el.set('href', PROVIDER_INFO[provider]['compare'] % (user, repo, commit1, commit2))
- el.text = md_util.AtomicString(text)
- el.set('class', 'magiclink magiclink-%s magiclink-compare' % provider)
- el.set(
- 'title',
- '%s %s: %s/%s@%s...%s' % (
- 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
- DIFF = 3
- REPO = 4
- USER = 5
- def __init__(self, md, base_url, base_user_url, labels, repo_shortner, social_shortener, excludes):
- """Initialize."""
- self.base = base_url
- self.repo_shortner = repo_shortner
- self.social_shortener = social_shortener
- self.base_user = base_user_url
- self.repo_labels = labels
- 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 = '%s' % user_repo
- link.text = md_util.AtomicString(text)
- if 'magiclink-repository' not in class_name:
- class_name.append('magiclink-repository')
- link.set(
- 'title',
- "%s %s: %s" % (
- 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('@' + user_repo)
- if 'magiclink-mention' not in class_name:
- class_name.append('magiclink-mention')
- link.set(
- 'title',
- "%s %s: %s" % (
- 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 = '%s...%s' % (value[0][0:hash_size], value[1][0:hash_size])
- elif self.my_user:
- text = '%s@%s...%s' % (user_repo.split('/')[1], value[0][0:hash_size], value[1][0:hash_size])
- else:
- text = '%s@%s...%s' % (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',
- '%s %s: %s@%s...%s' % (
- 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 = '%s@%s' % (user_repo.split('/')[1], value[0:hash_size])
- else:
- text = '%s@%s' % (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',
- '%s %s: %s@%s' % (label, repo_label, user_repo.rstrip('/'), value[0:hash_size])
- )
- def shorten_issue(self, link, class_name, label, user_repo, value, link_type):
- """Shorten issue/pull link."""
- # user/repo#(issue|pull)
- if link_type == self.ISSUE:
- issue_type = self.repo_labels.get('issue', 'Issue')
- separator = '#'
- if 'magiclink-issue' not in class_name:
- class_name.append('magiclink-issue')
- else:
- issue_type = self.repo_labels.get('pull', 'Pull Request')
- separator = '!'
- if 'magiclink-pull' not in class_name:
- class_name.append('magiclink-pull')
- if self.my_repo:
- text = separator + value
- elif self.my_user:
- text = user_repo.split('/')[1] + separator + value
- else:
- text = user_repo + separator + value
- link.text = md_util.AtomicString(text)
- link.set('title', '%s %s: %s%s%s' % (label, issue_type, user_repo.rstrip('/'), separator, value))
- def shorten_issue_commit(self, link, provider, link_type, user_repo, value, hash_size):
- """Shorten URL."""
- label = PROVIDER_INFO[provider]['provider']
- prov_class = 'magiclink-%s' % 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(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 = PROVIDER_INFO[provider]['provider']
- prov_class = 'magiclink-%s' % 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(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
- 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, 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
- def is_my_user(self, provider, match):
- """Check if link is from our specified user."""
- return self.base_user and match.group(provider + '_base').startswith(self.base_user)
- def excluded(self, provider, match):
- """Check if user has been excluded."""
- user = match.group(provider + '_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 = 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)
- found = True
- # All right, everything set, let's shorten.
- if not self.excluded(provider, m):
- self.shorten_issue_commit(
- link,
- provider,
- link_type,
- m.group(provider + '_user_repo'),
- value,
- 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)
- found = True
- if not self.excluded(provider, m):
- # All right, everything set, let's shorten.
- self.shorten_user_repo(
- link,
- provider,
- link_type,
- m.group(provider + '_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, 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://%s" % 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 "%s#%d;" % (md_util.AMP_SUBSTITUTE, code)
- def handleMatch(self, m, data):
- """Handle email link patterns."""
- el = etree.Element("a")
- email = self.unescape(m.group('mail'))
- href = "mailto:%s" % email
- el.text = md_util.AtomicString(''.join([self.email_encode(ord(c)) for c in email]))
- el.set("href", ''.join([md_util.AMP_SUBSTITUTE + '#%d;' % ord(c) 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', '%s/%s' % (PROVIDER_INFO[provider]['url'], mention))
- el.set(
- 'title',
- "%s %s: %s" % (PROVIDER_INFO[provider]['provider'], self.labels.get('mention', "User"), mention)
- )
- el.set('class', 'magiclink magiclink-%s magiclink-mention' % provider)
- el.text = md_util.AtomicString('@' + 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', '%s/%s/%s' % (PROVIDER_INFO[provider]['url'], user, repo))
- el.set(
- 'title',
- "%s %s: %s/%s" % (
- PROVIDER_INFO[provider]['provider'], self.labels.get('repository', 'Repository'), user, repo
- )
- )
- el.set('class', 'magiclink magiclink-%s magiclink-repository' % provider)
- el.text = md_util.AtomicString('%s/%s' % (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:
- self.process_issues(el, provider, user, repo, value)
- 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:
- self.process_issues(el, provider, user, repo, value)
- 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: {}"
- ],
- 'user': [
- '',
- 'The base user name to use - Default: ""'
- ],
- 'repo': [
- '',
- 'The base repo to use - Default: ""'
- ]
- }
- super(MagiclinkExtension, self).__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, int_mentions, ext_mentions, config):
- """Setup shorthand."""
- # Setup URL shortener
- escape_chars = ['@']
- util.escape_chars(md, escape_chars)
- # Repository shorthand
- if self.git_short:
- git_ext_repo = MagiclinkRepositoryPattern(
- RE_GIT_EXT_REPO_MENTIONS, md, self.user, self.repo, self.provider, self.labels
- )
- 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 % int_mentions, md, self.user, self.repo, self.provider, self.labels
- )
- md.inlinePatterns.register(git_int_repo, "magic-repo-int-mention", 79.8)
- # Mentions
- pattern = RE_ALL_EXT_MENTIONS % '|'.join(ext_mentions)
- git_mention = MagiclinkMentionPattern(
- pattern, md, self.user, self.repo, self.provider, self.labels
- )
- md.inlinePatterns.register(git_mention, "magic-ext-mention", 79.7)
- git_mention = MagiclinkMentionPattern(
- RE_INT_MENTIONS % int_mentions, md, self.user, self.repo, self.provider, self.labels
- )
- 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
- )
- 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 % int_mentions, md, self.user, self.repo, self.provider, self.labels
- )
- 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
- )
- 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):
- """Setup shortener."""
- shortener = MagicShortenerTreeprocessor(
- md, base_url, base_user_url, self.labels, repo_shortner, social_shortener,
- self.shortener_exclusions
- )
- 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 = '%s/%s/%s/' % (PROVIDER_INFO[self.provider]['url'], self.user, self.repo)
- base_user_url = '%s/%s/' % (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.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()}
- for key, value in config.get('shortener_user_exclude', {}).items():
- if key in ('github', 'bitbucket', 'gitlab', 'twitter') and isinstance(value, (list, tuple, set)):
- self.shortener_exclusions[key] = set([x.lower() for x in value])
- # Ensure valid provider
- if self.provider not in 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)
- # 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)
- def makeExtension(*args, **kwargs):
- """Return extension."""
- return MagiclinkExtension(*args, **kwargs)
|