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)