diff --git a/docs/src/markdown/extensions/magiclink.md b/docs/src/markdown/extensions/magiclink.md index af9db46b3..6ed07f1e2 100644 --- a/docs/src/markdown/extensions/magiclink.md +++ b/docs/src/markdown/extensions/magiclink.md @@ -13,7 +13,8 @@ implement (if desired) via the provided [classes](#css). User's are free to refe MagicLink is an extension that provides a number of useful link related features. MagicLink can auto-link HTML, FTP, and email links. It can auto-convert repository links (GitHub, GitLab, and Bitbucket) and display them in a more concise, -shorthand format. MagicLink can also be configured to directly auto-link the aforementioned shorthand format. +shorthand format. MagicLink can be configured to directly auto-link the aforementioned shorthand format. +MagicLink can also auto-link simple user-defined patterns for use cases like ticketing systems. If you happen to have some conflicts with syntax for a specific case, you can always revert to the old auto-link format as well: `#!md `. If enabled, repository link shortening will be applied to the angle bracketed @@ -479,6 +480,51 @@ and will copy them for your custom repository. If this is not sufficient, you ca `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. +## Simple User-Defined References + +You can integrate with additional services like ticketing systems or shortlink providers (sometimes called `go/` links) by defining simple patterns that will be autolinked. + +The `simplerefs` configuration supports matching a static prefix followed by an alphanumeric identifier. We look for patterns of form `{ref_prefix}{identifier}` with the following restrictions: + +- `ref_prefix` is a configured literal value consisting of a letter followed by alphanumeric characters or punctuation from the set `_/-`; it will usually end with punctuation like `TICKET-` +- `identifier` is an alphanumeric string containing at least one character; it will be captured and injected into a configured target URL +- The pattern must be preceeded by whitespace or be at the beginning of the line +- The pattern must be followed by whitespace or punctuation from the set `.,!?:` + +Suppose we have a Jira project called `TICKET` where the issues are referred to like `TICKET-123` and a shortlink service where labels look like `go/myproject`. Our configuration will be: + +``` +'simplerefs': [ + { + # Anchor tags will have class name "magiclink-simplerefs-ticket" + 'ref_prefix': 'TICKET-', + # The target must contain "" to indicate where to inject the captured identifier + 'target_url': 'https://ticket.test.com/TICKET-' + }, + { + # Anchor tags will have class name "magiclink-simplerefs-go" + 'ref_prefix': 'go/', + 'target_url': 'https://go.test.com/' + } +] +``` + +Each `ref_prefix` is paired with a `target_url` that must contain an `` placeholder where the captured identifier will be injected. Although matching is case-insensitive, `simplerefs` will preserve casing of the input text. Note that the `ref_prefix` is also used to derive a class name attached to the links it produces; the class name will be the value of `ref_prefix` downcased and with punctuation removed. + +To make this concrete, consider the following cases using the above config: + +Markdown Input | HTML Output +-------------- | ----------- +`TICKET-123` | `TICKET-123` +`ticket-123` | `ticket-123` +`go/myproject` | `go/myproject` +`go/my-project`| `go/my-project` (not matched due to `-` in identifier position) + +Users should be aware of the following restrictions: + +- Do not configure overlapping prefixes; for example, `TICKET` and `TICKETNUM` prefixes would both match `TICKETNUM123` so only one rule can be applied +- Do not configure prefixes that differ only in punctuation; `TICKET-` and `TICKET:` would both have the same derived class name, which will throw a configuration error + ## 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/magiclink.py b/pymdownx/magiclink.py index d4c450cce..81397d1e9 100644 --- a/pymdownx/magiclink.py +++ b/pymdownx/magiclink.py @@ -86,6 +86,15 @@ # Internal mention patterns RE_INT_MENTIONS = r'(?P(?%s) # prefix; to be injected + (?P\w+) # identifier + ) +''' + + def create_ext_mentions(name, provider_type): """Create external mentions by provider type.""" @@ -1036,6 +1045,33 @@ def handleMatch(self, m, data): return el, m.start(0), m.end(0) +class MagiclinkSimpleRefPattern(InlineProcessor): + """Return a link Element given a custom prefix.""" + + ANCESTOR_EXCLUDES = ('a',) + + def __init__(self, pattern, md, shortname, target_url): + """Initialize.""" + + self.shortname = shortname + self.target_url = target_url + InlineProcessor.__init__(self, pattern, md) + + def handleMatch(self, m, data): + """Return link.""" + + el = etree.Element("a") + prefix = m.group(1) + identifier = m.group(2) + el.set('href', self.target_url.replace('', identifier)) + el.text = prefix + identifier + + el.set('class', f'magiclink magiclink-simpleref magiclink-simpleref-{self.shortname}') + + return el, m.start(0), m.end(0) + + + class MagiclinkExtension(Extension): """Add auto link and link transformation extensions to Markdown class.""" @@ -1097,6 +1133,10 @@ def __init__(self, *args, **kwargs): 'custom': [ {}, "Custom repositories hosts - Default {}" + ], + 'simplerefs': [ + [], + "User-defined reference patterns - Default []" ] } super().__init__(*args, **kwargs) @@ -1115,6 +1155,17 @@ def setup_autolinks(self, md, config): md.inlinePatterns.register(MagiclinkMailPattern(RE_MAIL, md), "magic-mail", 84.9) + def setup_simplerefs(self, md, config): + """Setup user-defined simple reference replacements.""" + + for simpleref_config in config['simplerefs']: + ref_prefix = simpleref_config['ref_prefix'] + target_url = simpleref_config['target_url'] + pattern_re = RE_SIMPLEREF_TEMPLATE % ref_prefix + shortname = re.sub(r'\W+', '', ref_prefix).lower() + pattern = MagiclinkSimpleRefPattern(pattern_re, md, shortname, target_url) + md.inlinePatterns.register(pattern, "simpleref-" + shortname, 120) + def setup_shorthand(self, md): """Setup shorthand.""" @@ -1308,6 +1359,7 @@ def extendMarkdown(self, md): self.provider = 'github' self.setup_autolinks(md, config) + self.setup_simplerefs(md, config) if self.git_short or self.social_short: self.ext_mentions = [] diff --git a/tests/test_extensions/test_magiclink.py b/tests/test_extensions/test_magiclink.py index 4618ba3de..24bf00917 100644 --- a/tests/test_extensions/test_magiclink.py +++ b/tests/test_extensions/test_magiclink.py @@ -399,3 +399,65 @@ def test_deprecated_twitter_shortener(self): self.assertTrue(len(w) == 1) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) self.assertTrue(found) + +class TestMagicLinkSimpleRefs(util.MdCase): + """Test cases for autolinking of user-defined simple references.""" + + extension = [ + 'pymdownx.magiclink' + ] + + extension_configs = { + 'pymdownx.magiclink': { + 'simplerefs': [ + { + 'ref_prefix': 'TICKET-', + 'target_url': 'https://ticket.test.com/TICKET-' + }, + { + 'ref_prefix': 'go/', + 'target_url': 'https://go.test.com/' + }, + ] + } + } + + def test_numeric(self): + """Test numeric identifiers.""" + + self.check_markdown( + 'TICKET-123', + '

TICKET-123

' # noqa: E501 + ) + + def test_word_boundary(self): + """Test numeric identifiers.""" + + self.check_markdown( + 'Hello, TICKET-123!', + '

Hello, TICKET-123!

' # noqa: E501 + ) + + def test_alphanumeric(self): + """Test alphanumeric identifiers.""" + + self.check_markdown( + 'go/abc123', + '

go/abc123

' # noqa: E501 + ) + + def test_underscores(self): + """Test underscore counts as a word character.""" + + self.check_markdown( + 'go/abc_123', + '

go/abc_123

' # noqa: E501 + ) + + def test_hyphen(self): + """Test hyphen breaks matching.""" + + self.check_markdown( + 'go/abc-123', + '

go/abc-123

' # noqa: E501 + )