diff --git a/contrib/menusystem.py b/contrib/menusystem.py
index 492c1def5f..35b2a41990 100644
--- a/contrib/menusystem.py
+++ b/contrib/menusystem.py
@@ -313,9 +313,9 @@ class MenuNode(object):
choice = ""
if self.keywords[ilink]:
if self.keywords[ilink] not in (CMD_NOMATCH, CMD_NOINPUT):
- choice += "{g%s{n" % self.keywords[ilink]
+ choice += "{g{lc%s{lt%s{le{n" % (self.keywords[ilink], self.keywords[ilink])
else:
- choice += "{g %i{n" % (ilink + 1)
+ choice += "{g {lc%i{lt%i{le{n" % ((ilink + 1), (ilink + 1))
if self.linktexts[ilink]:
choice += " - %s" % self.linktexts[ilink]
choices.append(choice)
diff --git a/src/server/portal/mxp.py b/src/server/portal/mxp.py
new file mode 100644
index 0000000000..94eb3ac7f0
--- /dev/null
+++ b/src/server/portal/mxp.py
@@ -0,0 +1,57 @@
+"""
+MXP - Mud eXtension Protocol.
+
+Partial implementation of the MXP protocol.
+The MXP protocol allows more advanced formatting options for telnet clients
+that supports it (mudlet, zmud, mushclient are a few)
+
+This only implements the SEND tag.
+
+More information can be found on the following links:
+http://www.zuggsoft.com/zmud/mxp.htm
+http://www.mushclient.com/mushclient/mxp.htm
+http://www.gammon.com.au/mushclient/addingservermxp.htm
+"""
+import re
+
+LINKS_SUB = re.compile(r'\{lc(.*?)\{lt(.*?)\{le', re.DOTALL)
+
+MXP = "\x5B"
+MXP_TEMPSECURE = "\x1B[4z"
+MXP_SEND = MXP_TEMPSECURE + \
+ "" + \
+ "\\2" + \
+ MXP_TEMPSECURE + \
+ ""
+
+def mxp_parse(text):
+ """
+ Replaces links to the correct format for MXP.
+ """
+ text = LINKS_SUB.sub(MXP_SEND, text)
+ return text
+
+class Mxp(object):
+ """
+ Implements the MXP protocol.
+ """
+
+ def __init__(self, protocol):
+ """Initializes the protocol by checking if the client supports it."""
+ self.protocol = protocol
+ self.protocol.protocol_flags["MXP"] = False
+ self.protocol.will(MXP).addCallbacks(self.do_mxp, self.no_mxp)
+
+ def no_mxp(self, option):
+ """
+ Client does not support MXP.
+ """
+ self.protocol.protocol_flags["MXP"] = False
+
+ def do_mxp(self, option):
+ """
+ Client does support MXP.
+ """
+ self.protocol.protocol_flags["MXP"] = True
+ self.protocol.handshake_done()
+ self.protocol.requestNegotiation(MXP, '')
diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py
index 48d6b3fbe5..08247a849e 100644
--- a/src/server/portal/telnet.py
+++ b/src/server/portal/telnet.py
@@ -12,6 +12,7 @@ from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE,
from src.server.session import Session
from src.server.portal import ttype, mssp, msdp, naws
from src.server.portal.mccp import Mccp, mccp_compress, MCCP
+from src.server.portal.mxp import MXP, Mxp, mxp_parse
from src.utils import utils, ansi, logger
_RE_N = re.compile(r"\{n$")
@@ -47,6 +48,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
self.mssp = mssp.Mssp(self)
# msdp
self.msdp = msdp.Msdp(self)
+ # mxp support
+ self.mxp = Mxp(self)
# add this new connection to sessionhandler so
# the Server becomes aware of it.
self.sessionhandler.connect(self)
@@ -199,6 +202,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
given, ttype result is used. If
client does not suport xterm256, the
ansi fallback will be used
+ mxp=True/False - enforce mxp setting. If not given, enables if we
+ detected client support for it
ansi=True/False - enforce ansi setting. If not given,
ttype result is used.
nomarkup=True - strip all ansi markup (this is the same as
@@ -234,6 +239,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
nomarkup = kwargs.get("nomarkup", not (xterm256 or useansi))
prompt = kwargs.get("prompt")
echo = kwargs.get("echo", None)
+ mxp = kwargs.get("mxp", "MXP" in self.protocol_flags)
#print "telnet kwargs=%s, message=%s" % (kwargs, text)
#print "xterm256=%s, useansi=%s, raw=%s, nomarkup=%s, init_done=%s" % (xterm256, useansi, raw, nomarkup, ttype.get("init_done"))
@@ -244,7 +250,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
# we need to make sure to kill the color at the end in order
# to match the webclient output.
#print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self), "nomarkup: %s, xterm256: %s" % (nomarkup, xterm256)
- self.sendLine(ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=xterm256))
+ linetosend = ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=xterm256, mxp=mxp)
+ if mxp:
+ linetosend = mxp_parse(linetosend)
+ self.sendLine(linetosend)
if prompt:
# Send prompt separately
diff --git a/src/utils/ansi.py b/src/utils/ansi.py
index 07d0549081..53fa571164 100644
--- a/src/utils/ansi.py
+++ b/src/utils/ansi.py
@@ -172,7 +172,13 @@ class ANSIParser(object):
"""
return self.ansi_regex.sub("", string)
- def parse_ansi(self, string, strip_ansi=False, xterm256=False):
+ def strip_mxp(self, string):
+ """
+ Strips all MXP codes from a string.
+ """
+ return self.mxp_sub.sub(r'\2', string)
+
+ def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False):
"""
Parses a string, subbing color codes according to
the stored mapping.
@@ -196,6 +202,7 @@ class ANSIParser(object):
return _PARSE_CACHE[cachekey]
self.do_xterm256 = xterm256
+ self.do_mxp = mxp
in_string = utils.to_str(string)
# do string replacement
@@ -209,8 +216,12 @@ class ANSIParser(object):
if strip_ansi:
# remove all ansi codes (including those manually
# inserted in string)
+ parsed_string = self.strip_mxp(parsed_string)
return self.strip_raw_codes(parsed_string)
+ if not mxp:
+ parsed_string = self.strip_mxp(parsed_string)
+
# cache and crop old cache
_PARSE_CACHE[cachekey] = parsed_string
if len(_PARSE_CACHE) > _PARSE_CACHE_SIZE:
@@ -303,11 +314,14 @@ class ANSIParser(object):
(r'\{\[[0-5]{3}', "") # {[123 - background colour
]
+ mxp_re = r'\{lc(.*?)\{lt(.*?)\{le'
+
# prepare regex matching
#ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
# for sub in ansi_map]
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 mux_ansi_map + ext_ansi_map]), re.DOTALL)
+ mxp_sub = re.compile(mxp_re, re.DOTALL)
# used by regex replacer to correctly map ansi sequences
ansi_map = dict(mux_ansi_map + ext_ansi_map)
@@ -326,12 +340,12 @@ ANSI_PARSER = ANSIParser()
# Access function
#
-def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False):
+def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False, mxp=False):
"""
Parses a string, subbing color codes as needed.
"""
- return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256)
+ return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256, mxp=mxp)
def strip_raw_ansi(string, parser=ANSI_PARSER):
diff --git a/src/utils/text2html.py b/src/utils/text2html.py
index c091149468..cc430d427b 100644
--- a/src/utils/text2html.py
+++ b/src/utils/text2html.py
@@ -77,6 +77,7 @@ class TextToHTMLparser(object):
re_hilite = re.compile("(?:%s)(.*)(?=%s)" % (hilite.replace("[", r"\["), fgstop))
re_uline = re.compile("(?:%s)(.*?)(?=%s)" % (ANSI_UNDERLINE.replace("[", r"\["), fgstop))
re_string = re.compile(r'(?P[<&>])|(?P [ \t]+)|(?P\r\n|\r|\n)', re.S|re.M|re.I)
+ re_link = re.compile(r'\{lc(.*?)\{lt(.*?)\{le', re.DOTALL)
def re_color(self, text):
"""
@@ -119,6 +120,13 @@ class TextToHTMLparser(object):
# change pages (and losing our webclient session).
return re.sub(regexp, r'\1', text)
+ def convert_links(self, text):
+ """
+ Replaces links with HTML code
+ """
+ html = "\\2"
+ return self.re_link.sub(html, text)
+
def do_sub(self, m):
"Helper method to be passed to re.sub."
c = m.groupdict()
@@ -139,7 +147,7 @@ class TextToHTMLparser(object):
ansi codes into html statements.
"""
# parse everything to ansi first
- text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=False)
+ text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=False, mxp=True)
# convert all ansi to html
result = re.sub(self.re_string, self.do_sub, text)
result = self.re_color(result)
@@ -149,6 +157,7 @@ class TextToHTMLparser(object):
result = self.convert_linebreaks(result)
result = self.remove_backspaces(result)
result = self.convert_urls(result)
+ result = self.convert_links(result)
# clean out eventual ansi that was missed
#result = parse_ansi(result, strip_ansi=True)