diff --git a/CHANGELOG.md b/CHANGELOG.md
index e45bc07133..6775456698 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,6 +66,7 @@ Up requirements to Django 3.2+
into a more consistent structure for overriding. Expanded webpage documentation considerably.
- REST API list-view was shortened (#2401). New CSS/HTML. Add ReDoc for API autodoc page.
- Update and fix dummyrunner with cleaner code and setup.
+- Added an MXP anchor tag in addition to the command tag
### Evennia 0.9.5 (2019-2020)
diff --git a/docs/source/Concepts/Clickable-Links.md b/docs/source/Concepts/Clickable-Links.md
index b29065e9e2..5a4f9ed262 100644
--- a/docs/source/Concepts/Clickable-Links.md
+++ b/docs/source/Concepts/Clickable-Links.md
@@ -1,11 +1,12 @@
## Clickable links
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
-clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
-the webclient or an third-party telnet client with [MXP](http://www.zuggsoft.com/zmud/mxp.htm)
-support (*Note: Evennia only supports clickable links, no other MXP features*).
+clicked by a mouse and either trigger a given Evennia command, or open a URL in an external web
+browser. To support clickable links, Evennia requires the webclient or an third-party telnet client
+with [MXP](http://www.zuggsoft.com/zmud/mxp.htm) support (*Note: Evennia only supports clickable links, no other MXP features*).
- `|lc` to start the link, by defining the command to execute.
+- `|lu` to start the link, by defining the URL to open.
- `|lt` to continue with the text to show to the user (the link text).
- `|le` to end the link text and the link definition.
diff --git a/evennia/server/portal/mxp.py b/evennia/server/portal/mxp.py
index 6c938ab709..7f5b39d392 100644
--- a/evennia/server/portal/mxp.py
+++ b/evennia/server/portal/mxp.py
@@ -16,12 +16,14 @@ http://www.gammon.com.au/mushclient/addingservermxp.htm
import re
LINKS_SUB = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
+URL_SUB = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
# MXP Telnet option
MXP = bytes([91]) # b"\x5b"
MXP_TEMPSECURE = "\x1B[4z"
MXP_SEND = MXP_TEMPSECURE + '' + "\\2" + MXP_TEMPSECURE + ""
+MXP_URL = MXP_TEMPSECURE + '' + "\\2" + MXP_TEMPSECURE + ""
def mxp_parse(text):
@@ -38,6 +40,7 @@ def mxp_parse(text):
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
text = LINKS_SUB.sub(MXP_SEND, text)
+ text = URL_SUB.sub(MXP_URL, text)
return text
diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py
index 0dd2d8234c..490621329c 100644
--- a/evennia/utils/ansi.py
+++ b/evennia/utils/ansi.py
@@ -223,6 +223,7 @@ class ANSIParser(object):
ansi_xterm256_bright_bg_map += settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
mxp_re = r"\|lc(.*?)\|lt(.*?)\|le"
+ mxp_url_re = r"\|lu(.*?)\|lt(.*?)\|le"
# prepare regex matching
brightbg_sub = re.compile(
@@ -237,6 +238,7 @@ class ANSIParser(object):
# xterm256_sub = re.compile(r"|".join([tup[0] for tup in xterm256_map]), re.DOTALL)
ansi_sub = re.compile(r"|".join([re.escape(tup[0]) for tup in ansi_map]), re.DOTALL)
mxp_sub = re.compile(mxp_re, re.DOTALL)
+ mxp_url_sub = re.compile(mxp_url_re, re.DOTALL)
# used by regex replacer to correctly map ansi sequences
ansi_map_dict = dict(ansi_map)
@@ -424,7 +426,9 @@ class ANSIParser(object):
string (str): The processed string.
"""
- return self.mxp_sub.sub(r"\2", string)
+ string = self.mxp_sub.sub(r"\2", string)
+ string = self.mxp_url_sub.sub(r"\2", string)
+ return string
def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False):
"""
diff --git a/evennia/utils/tests/test_tagparsing.py b/evennia/utils/tests/test_tagparsing.py
index 5c47bc5b6a..ae78c8fd0d 100644
--- a/evennia/utils/tests/test_tagparsing.py
+++ b/evennia/utils/tests/test_tagparsing.py
@@ -145,13 +145,17 @@ class ANSIStringTestCase(TestCase):
"""
mxp1 = "|lclook|ltat|le"
mxp2 = "Start to |lclook here|ltclick somewhere here|le first"
+ mxp3 = "Check out |luhttps://www.example.com|ltmy website|le!"
self.assertEqual(15, len(ANSIString(mxp1)))
self.assertEqual(53, len(ANSIString(mxp2)))
+ self.assertEqual(53, len(ANSIString(mxp3)))
# These would indicate an issue with the tables.
self.assertEqual(len(ANSIString(mxp1)), len(ANSIString(mxp1).split("\n")[0]))
self.assertEqual(len(ANSIString(mxp2)), len(ANSIString(mxp2).split("\n")[0]))
+ self.assertEqual(len(ANSIString(mxp3)), len(ANSIString(mxp3).split("\n")[0]))
self.assertEqual(mxp1, ANSIString(mxp1))
self.assertEqual(mxp2, str(ANSIString(mxp2)))
+ self.assertEqual(mxp3, str(ANSIString(mxp3)))
def test_add(self):
"""
diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py
index 7fb19a8ab8..bb4251caf7 100644
--- a/evennia/utils/text2html.py
+++ b/evennia/utils/text2html.py
@@ -103,9 +103,10 @@ class TextToHTMLparser(object):
)
re_dblspace = re.compile(r" {2,}", re.M)
re_url = re.compile(
- r'((?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
+ r'(?\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
)
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
+ re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
def _sub_bgfg(self, colormatch):
# print("colormatch.groups()", colormatch.groups())
@@ -290,6 +291,21 @@ class TextToHTMLparser(object):
)
return val
+ def sub_mxp_urls(self, match):
+ """
+ Helper method to be passed to re.sub,
+ replaces MXP links with HTML code.
+ Args:
+ match (re.Matchobject): Match for substitution.
+ Returns:
+ text (str): Processed text.
+ """
+ url, text = [grp.replace('"', "\\"") for grp in match.groups()]
+ val = (
+ r"""{text}""".format(url=url, text=text)
+ )
+ return val
+
def sub_text(self, match):
"""
Helper method to be passed to re.sub,
@@ -337,6 +353,7 @@ class TextToHTMLparser(object):
# convert all ansi to html
result = re.sub(self.re_string, self.sub_text, text)
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
+ result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
result = self.re_color(result)
result = self.re_bold(result)
result = self.re_underline(result)