# -*- coding=UTF-8; tab-width: 4 -*- from .unicolor import Color import re global basestr basestr = str try: basestr = str except NameError: # We're running Python 3. Leave it be. pass # Yanked from the old Textsub file. Pardon the mess. # TODO: Need to consider letting conversions register themselves - or, as a # simpler solution, just have CTag.convert and have it search for a conversion # function appropriate to the given format - e.g. CTag.convert_pchum. class Lexeme(object): def __init__(self, string, origin): # The 'string' property is just what it came from; the original # representation. It doesn't have to be used, and honestly probably # shouldn't be. self.string = string self.origin = origin def __str__(self): ##return self.string return self.convert(self.origin) def __len__(self): ##return len(self.string) return len(str(self)) def convert(self, format): # This is supposed to be overwritten by subclasses raise NotImplementedError def rebuild(self, format): """Builds a copy of the owning Lexeme as if it had 'come from' a different original format, and returns the result.""" # TODO: This. Need to decide whether overloading will be required for # nearly every single subclass.... raise NotImplementedError @classmethod def from_mo(cls, mo, origin): raise NotImplementedError class Message(Lexeme): """An object designed to represent a message, possibly containing Lexeme objects in their native form as well. Intended to be a combination of a list and a string, combining the former with the latter's methods.""" def __init__(self, contents, origin): lexer = Lexer.lexer_for(origin)() working = lexer.lex(contents) # TODO: Rebuild all lexemes so that they all 'come from' the same # format (e.g. their .origin values are all the same as the Message's). for i, elt in enumerate(working): try: # Try to rebuild for the new format elt = elt.rebuild(origin) except AttributeError: # It doesn't let us rebuild, so it's probably not a Lexeme continue else: # Assign it to the proper place, replacing the old one working[i] = elt self.origin = origin self.contents = working self.string = "".join(lexer.list_convert(working)) # TODO: Finish all the rest of this. class Specifier(Lexeme): # Almost purely for classification at present sets_color = sets_bold = sets_italic = sets_underline = None resets_color = resets_bold = resets_italic = resets_underline = None resets_formatting = None # If this form has a more compact form, use it compact = False # Made so that certain odd message-ish things have a place to go. May have its # class changed later. class Chunk(Specifier): pass class FTag(Specifier): pass class CTag(Specifier): """Denotes the beginning or end of a color change.""" sets_color = True def __init__(self, string, origin, color): super(CTag, self).__init__(string, origin) # So we can also have None if isinstance(color, tuple): if len(color) < 2: raise ValueError self.color, self.bg_color = color[:2] else: self.color = color self.bg_color = None def has_color(self): if self.color is not None or self.bg_color is not None: return True return False def convert(self, format): text = "" color = self.color bg = self.bg_color if format == "irc": # Put in the control character for a color code. text = "\x03" if color: text += color.ccode if bg: text += "," + bg.ccode elif bg: text += "99," + bg.ccode elif format == "pchum": if not color: text = "" else: if color.name: text = "" % color.name else: # We should have a toggle here, just in case this isn't # acceptable for some reason, but it usually will be. rgb = "" % color.to_rgb_tuple() hxs = color.hexstr if self.compact: # Try to crush it down even further. hxs = color.reduce_hexstr(hxs) hxs = "" % hxs if len(rgb) <= len(hxs): # Prefer the more widely-recognized default format text = rgb else: # Hex is shorter, and recognized by everything thus # far; use it. text = hxs elif format == "plaintext": text = "" return text @classmethod def from_mo(cls, mo, origin): inst = None if origin == "irc": text = mo.group() fg, bg = mo.groups() try: fg = Color("\x03" + fg) except: fg = None try: bg = Color("\x03" + bg) except: bg = None inst = cls(text, origin, color=(fg, bg)) elif origin == "pchum": text = mo.group() inst = cls(text, origin, color=None) if mo.lastindex: text = mo.group(1) cmatch = Pesterchum._ctag_rgb.match(text) if cmatch: working = cmatch.groups() working = list(map(int, working)) inst.color = Color(*working) else: try: inst.color = Color(text) except: pass return inst class CTagEnd(CTag): # TODO: Make this a separate class - NOT a subclass of CTag like it is at # present resets_color = True def convert(self, format): text = "" if format == "irc": return "\x03" elif format == "pchum": return "" elif format == "plaintext": return "" return text def has_color(self): return False @classmethod def from_mo(cls, mo, origin): # Turns the whole match into it (for now) return cls(mo.group(), origin, color=None) class LineColor(CTag): pass class LineColorEnd(CTagEnd): pass class FTagEnd(Specifier): resets_formatting = True class ResetTag(CTagEnd, FTagEnd): def convert(self, format): text = "" if format == "irc": return "\x0F" elif format == "pchum": # Later on, this one is going to be infuriatingly tricky. # Supporting things like bold and so on doesn't really allow for an # easy 'reset' tag. # I *could* implement it, and it wouldn't be too hard, but it would # probably confuse more people than it helped. return "" elif format == "plaintext": return "" return text class SpecifierEnd(CTagEnd, FTagEnd): # This might not ever even be used, but you never know.... # If it does, we may need properties such as .resets_color, .resets_bold, # and so on and so forth pass # TODO: Consider using a metaclass to check those properties - e.g. if # a class .sets_color and a subclass .resets_color, set the subclass's # .sets_color to False class Lexer(object): # Subclasses need to supply a ref themselves ref = None compress_tags = False def breakdown(self, string, objlist): if not isinstance(string, basestr): msglist = string else: msglist = [string] for obj, rxp in objlist: working = [] for i, msg in enumerate(msglist): if not isinstance(msg, basestr): # We've probably got a tag or something else that we can't # actually parse into a tag working.append(msg) continue # If we got here, we have a string to parse oend = 0 for mo in rxp.finditer(msg): start, end = mo.span() if oend != start: # There's text between the end of the last match and # the beginning of this one, add it working.append(msg[oend:start]) tag = obj.from_mo(mo, origin=self.ref) working.append(tag) oend = end # We've finished parsing every match, check if there's any text # left if oend < len(msg): # There is; add it to the end of the list working.append(msg[oend:]) # Exchange the old list with the processed one, and continue msglist = working return msglist def lex(self, string): # Needs to be implemented by subclasses return self.breakdown(string, []) def list_convert(self, target, format=None): if format is None: format = self.ref converted = [] for elt in target: if isinstance(elt, Lexeme): elt = elt.convert(format) if not isinstance(elt, basestr): # Tempted to make this toss an error, but for now, we'll be # safe and make it convert to str elt = str(elt) converted.append(elt) return converted class Pesterchum(Lexer): ref = "pchum" _ctag_begin = re.compile(r"", flags=re.I) _ctag_rgb = re.compile(r"(\d+),(\d+),(\d+)") _ctag_end = re.compile(r"", flags=re.I) _mecmdre = re.compile(r"^(/me|PESTERCHUM:ME)(\S*)") # TODO: At some point, this needs to have support for setting up # optimization controls - so ctags will be rendered down into things like # "" instead of "". # I'd make this the default, but I'd like to retain *some* compatibility # with Chumdroid's faulty implementation...or at least have the option to. def lex(self, string): lexlist = [ ##(mecmd, self._mecmdre), (CTag, self._ctag_begin), ##(CTag, self._ctag_end) (CTagEnd, self._ctag_end), ] lexed = self.breakdown(string, lexlist) balanced = [] beginc = 0 endc = 0 for o in lexed: if isinstance(o, CTag): ##if o: if o.has_color(): # This means it has a color of some sort # TODO: Change this; pesterchum doesn't support BG colors, # so we should only check FG ones (has_color() checks both) # TODO: Consider making a Lexer method that checks if # a provided object would actually contribute something # when rendered under a certain format beginc += 1 elif beginc >= endc: endc += 1 # Apply compression, if we're set to. We made these objects, so # that should be okay. if self.compress_tags: o.compact = True balanced.append(o) # Original (Pesterchum) code: ##if isinstance(o, colorBegin): ## beginc += 1 ## balanced.append(o) ##elif isinstance(o, colorEnd): ## if beginc >= endc: ## endc += 1 ## balanced.append(o) ## else: ## balanced.append(o.string) ##else: ## balanced.append(o) # This will need to be re-evaluated to support the line end lexeme/etc. if beginc > endc: for i in range(0, beginc - endc): ##balanced.append(colorEnd("")) balanced.append(CTagEnd("", self.ref, None)) return balanced # TODO: Let us contextually set compression here or something, ugh. If # 'None' assume the self-set one. def list_convert(self, target, format=None): if format is None: format = self.ref converted = [] cstack = [] ##closecolor = lambda: converted.append(CTagEnd("", self.ref, None)) closecolor = lambda: converted.append( CTagEnd("", self.ref, None).convert(format) ) for elt in target: if isinstance(elt, LineColorEnd): # Go down the stack until we have a line color TO end while cstack: # Add a since we'll need one anyway closecolor() ##if isinstance(color, LineColor): if isinstance(cstack.pop(), LineColor): # We found what we wanted, and the color # was already popped from the stack, so # we're good # Breaking here allows it to be appended break continue elif isinstance(elt, ResetTag): # If it says reset, reset - which means go down the # stack to the most recent line color. while cstack: color = cstack[-1] if not isinstance(color, LineColor): # It's not a line color, so remove it del cstack[-1] # Add a closecolor() else: # It's a line color, so stop searching. # Using break here prevents the 'else' # clause of this while statement from # executing, which means that we go on to # add this to the result. break else: # We don't have any more entries in the stack; # just continue. continue ## We found the line color, so add it and continue ##converted.append(color.convert(format)) continue ## TODO: Make this actually add the reset char # The above shouldn't be necessary because this is Pesterchum's # format, not IRC's elif isinstance(elt, CTagEnd): try: color = cstack[-1] # Remove the oldest color, the one we're exiting if not isinstance(color, LineColor): # If we got here, we don't have a line color, # so we're free to act as usual cstack.pop() # Fetch the current nested color color = cstack[-1] else: # We have a line color and the current lexeme # is NOT a line color end; don't even bother # adding it to the processed result continue except LookupError: # We aren't nested in a color anymore # Passing here causes us to fall through to normal # handling pass # Not necessary due to Pesterchum's format ##else: ## # We're still nested.... ## ##converted.append(elt.convert(format)) ## converted.append(color.convert(format)) ## # We already added to the working list, so just ## # skip the rest ## continue elif isinstance(elt, CTag): # Push the color onto the stack - we're nested in it now cstack.append(elt) # Falling through adds it to the converted result if isinstance(elt, Lexeme): elt = elt.convert(format) elif not isinstance(elt, basestr): # Tempted to make this toss an error, but for now, we'll be # safe and make it convert to str elt = str(elt) converted.append(elt) return converted class RelayChat(Lexer): ref = "irc" # This could use some cleaning up later, but it'll work for now, hopefully ##_ccode_rxp = re.compile(r"\x03(?P\d\d?)?(?(fg),(?P\d\d?))?|\x0F") _ccode_rxp = re.compile(r"\x03(?P\d\d?)(?(fg),(?P\d\d?))?") _ccode_end_rxp = re.compile(r"\x03(?!\d\d?)") _reset_rxp = re.compile(r"\x0F") def lex(self, string): ##lexlist = [(CTag, self._ccode_rxp)] lexlist = [ (CTag, self._ccode_rxp), (CTagEnd, self._ccode_end_rxp), (ResetTag, self._reset_rxp), ] lexed = self.breakdown(string, lexlist) # Don't bother with the whole fancy color-balancing thing yet return lexed def list_convert(self, target, format=None): if format is None: format = self.ref converted = [] cstack = [] for elt in target: if isinstance(elt, CTag): if isinstance(elt, CTagEnd) or not elt.has_color(): if isinstance(elt, LineColorEnd): # Go down the stack until we have a line color TO # end while cstack: ##if isinstance(color, LineColor): if isinstance(cstack.pop(), LineColor): # We found what we wanted, and the color # was already popped from the stack, so # we're good break # The current lexeme isn't a line color end elif isinstance(elt, ResetTag): # If it says reset, reset - which means go down the # stack to the most recent line color. while cstack: color = cstack[-1] if not isinstance(color, LineColor): # It's not a line color, so remove it del cstack[-1] else: # It's a line color, so stop searching. # Using break here prevents the 'else' # clause of this while statement from # executing. break else: # We don't have any more entries in the stack; # just continue. continue # We found the line color, so add it and continue converted.append(color.convert(format)) continue # TODO: Make this actually add the reset char else: try: color = cstack[-1] # Remove the oldest color, the one we're exiting if not isinstance(color, LineColor): # If we got here, we don't have a line color, # so we're free to act as usual cstack.pop() # Fetch the current nested color color = cstack[-1] else: # We have a line color and the current lexeme # is NOT a line color end; don't even bother # adding it to the processed result continue except LookupError: # We aren't nested in a color anymore # Passing here causes us to fall through to normal # handling pass else: # We're still nested.... ##converted.append(elt.convert(format)) converted.append(color.convert(format)) # We already added to the working list, so just # skip the rest continue else: # Push the color onto the stack - we're nested in it now cstack.append(elt) if isinstance(elt, Lexeme): elt = elt.convert(format) elif not isinstance(elt, basestr): # Tempted to make this toss an error, but for now, we'll be # safe and make it convert to str elt = str(elt) converted.append(elt) return converted def _list_convert_new(self, target, format=None): if format is None: format = self.ref converted = [] cstack = [] for elt in target: if isinstance(elt, LineColorEnd): # Go down the stack until we have a line color TO end while cstack: # Add a since we'll need one anyway # Is closecolor accessible here? try: closecolor() except Exception as e: print(e) ##if isinstance(color, LineColor): if isinstance(cstack.pop(), LineColor): # We found what we wanted, and the color # was already popped from the stack, so # we're good # Breaking here allows it to be appended break continue elif isinstance(elt, ResetTag): # If it says reset, reset - which means go down the # stack to the most recent line color. while cstack: color = cstack[-1] if not isinstance(color, LineColor): # It's not a line color, so remove it del cstack[-1] # Add a # Is closecolor accessible here? try: closecolor() except Exception as e: print(e) else: # It's a line color, so stop searching. # Using break here prevents the 'else' # clause of this while statement from # executing. break else: # We don't have any more entries in the stack; # just continue. continue ## We found the line color, so add it and continue ##converted.append(color.convert(format)) continue ## TODO: Make this actually add the reset char # The above shouldn't be necessary because this is Pesterchum's # format, not IRC's elif isinstance(elt, CTagEnd): try: color = cstack[-1] # Remove the oldest color, the one we're exiting if not isinstance(color, LineColor): # If we got here, we don't have a line color, # so we're free to act as usual cstack.pop() # Fetch the current nested color color = cstack[-1] else: # We have a line color and the current lexeme # is NOT a line color end; don't even bother # adding it to the processed result continue except LookupError: # We aren't nested in a color anymore # Passing here causes us to fall through to normal # handling pass # Not necessary due to Pesterchum's format ##else: ## # We're still nested.... ## ##converted.append(elt.convert(format)) ## converted.append(color.convert(format)) ## # We already added to the working list, so just ## # skip the rest ## continue elif isinstance(elt, CTag): # Push the color onto the stack - we're nested in it now cstack.append(elt) # Falling through adds it to the converted result if isinstance(elt, Lexeme): elt = elt.convert(format) elif not isinstance(elt, basestr): # Tempted to make this toss an error, but for now, we'll be # safe and make it convert to str elt = str(elt) converted.append(elt) return converted