From cc0329cdc45810f0ebcd465ed2935d6dc58bd7b1 Mon Sep 17 00:00:00 2001 From: Klukas Date: Mon, 16 Dec 2024 21:00:40 -0500 Subject: [PATCH 1/3] Initial implementation of customrefs --- pymdownx/magiclink.py | 52 +++++++++++++++++++++ tests/test_extensions/test_magiclink.py | 62 +++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/pymdownx/magiclink.py b/pymdownx/magiclink.py index d4c450cce..c60ff45b4 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 MagiclinkCustomRefPattern(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-customref magiclink-customref-{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 {}" + ], + 'custom_refs': [ + [], + "Custom 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_custom_refs(self, md, config): + """Setup custom refs.""" + + for custom_ref_config in config.get('custom_refs', []): + ref_prefix = custom_ref_config['ref_prefix'] + target_url = custom_ref_config['target_url'] + pattern_re = RE_CUSTOM_REFS_TEMPLATE % ref_prefix + shortname = re.sub(r'\W+', '', ref_prefix).lower() + pattern = MagiclinkCustomRefPattern(pattern_re, md, shortname, target_url) + md.inlinePatterns.register(pattern, "customref-" + 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_custom_refs(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..8774af350 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 TestMagicLinkCustomRefs(util.MdCase): + """Test cases for custom references.""" + + extension = [ + 'pymdownx.magiclink' + ] + + extension_configs = { + 'pymdownx.magiclink': { + 'custom_refs': [ + { + '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 + ) From f941e45e977cfcffb508666656b0551e3eb80474 Mon Sep 17 00:00:00 2001 From: Klukas Date: Wed, 18 Dec 2024 09:45:45 -0500 Subject: [PATCH 2/3] Change name to "simplerefs" instead of "custom_refs" --- pymdownx/magiclink.py | 28 ++++++++++++------------- tests/test_extensions/test_magiclink.py | 16 +++++++------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pymdownx/magiclink.py b/pymdownx/magiclink.py index c60ff45b4..81397d1e9 100644 --- a/pymdownx/magiclink.py +++ b/pymdownx/magiclink.py @@ -87,7 +87,7 @@ RE_INT_MENTIONS = r'(?P(?%s) # prefix; to be injected (?P\w+) # identifier @@ -1045,7 +1045,7 @@ def handleMatch(self, m, data): return el, m.start(0), m.end(0) -class MagiclinkCustomRefPattern(InlineProcessor): +class MagiclinkSimpleRefPattern(InlineProcessor): """Return a link Element given a custom prefix.""" ANCESTOR_EXCLUDES = ('a',) @@ -1066,7 +1066,7 @@ def handleMatch(self, m, data): el.set('href', self.target_url.replace('', identifier)) el.text = prefix + identifier - el.set('class', f'magiclink magiclink-customref magiclink-customref-{self.shortname}') + el.set('class', f'magiclink magiclink-simpleref magiclink-simpleref-{self.shortname}') return el, m.start(0), m.end(0) @@ -1134,9 +1134,9 @@ def __init__(self, *args, **kwargs): {}, "Custom repositories hosts - Default {}" ], - 'custom_refs': [ + 'simplerefs': [ [], - "Custom reference patterns - Default []" + "User-defined reference patterns - Default []" ] } super().__init__(*args, **kwargs) @@ -1155,16 +1155,16 @@ def setup_autolinks(self, md, config): md.inlinePatterns.register(MagiclinkMailPattern(RE_MAIL, md), "magic-mail", 84.9) - def setup_custom_refs(self, md, config): - """Setup custom refs.""" + def setup_simplerefs(self, md, config): + """Setup user-defined simple reference replacements.""" - for custom_ref_config in config.get('custom_refs', []): - ref_prefix = custom_ref_config['ref_prefix'] - target_url = custom_ref_config['target_url'] - pattern_re = RE_CUSTOM_REFS_TEMPLATE % ref_prefix + 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 = MagiclinkCustomRefPattern(pattern_re, md, shortname, target_url) - md.inlinePatterns.register(pattern, "customref-" + shortname, 120) + pattern = MagiclinkSimpleRefPattern(pattern_re, md, shortname, target_url) + md.inlinePatterns.register(pattern, "simpleref-" + shortname, 120) def setup_shorthand(self, md): """Setup shorthand.""" @@ -1359,7 +1359,7 @@ def extendMarkdown(self, md): self.provider = 'github' self.setup_autolinks(md, config) - self.setup_custom_refs(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 8774af350..24bf00917 100644 --- a/tests/test_extensions/test_magiclink.py +++ b/tests/test_extensions/test_magiclink.py @@ -400,8 +400,8 @@ def test_deprecated_twitter_shortener(self): self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) self.assertTrue(found) -class TestMagicLinkCustomRefs(util.MdCase): - """Test cases for custom references.""" +class TestMagicLinkSimpleRefs(util.MdCase): + """Test cases for autolinking of user-defined simple references.""" extension = [ 'pymdownx.magiclink' @@ -409,7 +409,7 @@ class TestMagicLinkCustomRefs(util.MdCase): extension_configs = { 'pymdownx.magiclink': { - 'custom_refs': [ + 'simplerefs': [ { 'ref_prefix': 'TICKET-', 'target_url': 'https://ticket.test.com/TICKET-' @@ -427,7 +427,7 @@ def test_numeric(self): self.check_markdown( 'TICKET-123', - '

TICKET-123

' # noqa: E501 + '

TICKET-123

' # noqa: E501 ) def test_word_boundary(self): @@ -435,7 +435,7 @@ def test_word_boundary(self): self.check_markdown( 'Hello, TICKET-123!', - '

Hello, TICKET-123!

' # noqa: E501 + '

Hello, TICKET-123!

' # noqa: E501 ) def test_alphanumeric(self): @@ -443,7 +443,7 @@ def test_alphanumeric(self): self.check_markdown( 'go/abc123', - '

go/abc123

' # noqa: E501 + '

go/abc123

' # noqa: E501 ) def test_underscores(self): @@ -451,7 +451,7 @@ def test_underscores(self): self.check_markdown( 'go/abc_123', - '

go/abc_123

' # noqa: E501 + '

go/abc_123

' # noqa: E501 ) def test_hyphen(self): @@ -459,5 +459,5 @@ def test_hyphen(self): self.check_markdown( 'go/abc-123', - '

go/abc-123

' # noqa: E501 + '

go/abc-123

' # noqa: E501 ) From b764f62c45e4f8ef61cca67830f3dc061268bc23 Mon Sep 17 00:00:00 2001 From: Klukas Date: Thu, 19 Dec 2024 09:09:01 -0500 Subject: [PATCH 3/3] Add documentation for simplerefs as a proposed spec --- docs/src/markdown/extensions/magiclink.md | 48 ++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) 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.