diff --git a/convo.py b/convo.py index 2b5affd..c386cf4 100644 --- a/convo.py +++ b/convo.py @@ -12,6 +12,9 @@ from dataobjs import PesterProfile, PesterHistory from generic import PesterIcon from parsetools import convertTags, lexMessage, splitMessage, mecmd, colorBegin, colorEnd, \ img2smiley, smiledict, oocre +import parsetools + +import pnc.lexercon as lexercon class PesterTabWindow(QtGui.QFrame): def __init__(self, mainwindow, parent=None, convo="convo"): @@ -721,36 +724,12 @@ class PesterConvo(QtGui.QFrame): @QtCore.pyqtSlot() def sentMessage(self): + # Offloaded to another function, like its sisters. + # Fetch the raw text from the input box. + text = self.textInput.text() text = unicode(self.textInput.text()) - if text == "" or text[0:11] == "PESTERCHUM:": - return - oocDetected = oocre.match(text.strip()) - if self.ooc and not oocDetected: - text = "(( %s ))" % (text) - self.history.add(text) - quirks = self.mainwindow.userprofile.quirks - lexmsg = lexMessage(text) - if type(lexmsg[0]) is not mecmd and self.applyquirks and not (self.ooc or oocDetected): - try: - lexmsg = quirks.apply(lexmsg) - except: - msgbox = QtGui.QMessageBox() - msgbox.setText("Whoa there! There seems to be a problem.") - msgbox.setInformativeText("A quirk seems to be having a problem. (Possibly you're trying to capture a non-existant group?)") - msgbox.exec_() - return - lexmsgs = splitMessage(lexmsg) - for lm in lexmsgs: - serverMsg = copy(lm) - self.addMessage(lm, True) - # if ceased, rebegin - if hasattr(self, 'chumopen') and not self.chumopen: - self.mainwindow.newConvoStarted.emit(QtCore.QString(self.title()), True) - self.setChumOpen(True) - text = convertTags(serverMsg, "ctag") - self.messageSent.emit(text, self.title()) - self.textInput.setText("") + return parsetools.kxhandleInput(self, text, flavor="convo") @QtCore.pyqtSlot() def addThisChum(self): diff --git a/memos.py b/memos.py index 8aa2350..e28351f 100644 --- a/memos.py +++ b/memos.py @@ -10,6 +10,7 @@ from generic import PesterIcon, RightClickList, mysteryTime from convo import PesterConvo, PesterInput, PesterText, PesterTabWindow from parsetools import convertTags, addTimeInitial, timeProtocol, \ lexMessage, colorBegin, colorEnd, mecmd, smiledict, oocre +import parsetools from logviewer import PesterLogViewer def delta2txt(d, format="pc"): @@ -805,36 +806,9 @@ class PesterMemo(PesterConvo): @QtCore.pyqtSlot() def sentMessage(self): text = unicode(self.textInput.text()) - if text == "" or text[0:11] == "PESTERCHUM:": - return - oocDetected = oocre.match(text.strip()) - if self.ooc and not oocDetected: - text = "(( %s ))" % (text) - self.history.add(text) - if self.time.getTime() == None: - self.sendtime() - grammar = self.time.getGrammar() - quirks = self.mainwindow.userprofile.quirks - lexmsg = lexMessage(text) - if type(lexmsg[0]) is not mecmd: - if self.applyquirks and not (self.ooc or oocDetected): - lexmsg = quirks.apply(lexmsg) - initials = self.mainwindow.profile().initials() - colorcmd = self.mainwindow.profile().colorcmd() - clientMsg = [colorBegin("" % (colorcmd), colorcmd), - "%s%s%s: " % (grammar.pcf, initials, grammar.number)] + lexmsg + [colorEnd("")] - # account for TC's parsing error - serverMsg = [colorBegin("" % (colorcmd), colorcmd), - "%s: " % (initials)] + lexmsg + [colorEnd(""), " "] - else: - clientMsg = copy(lexmsg) - serverMsg = copy(lexmsg) - - self.addMessage(clientMsg, True) - serverText = convertTags(serverMsg, "ctag") - self.messageSent.emit(serverText, self.title()) - - self.textInput.setText("") + + return parsetools.kxhandleInput(self, text, flavor="memos") + @QtCore.pyqtSlot(QtCore.QString) def namesUpdated(self, channel): c = unicode(channel) diff --git a/menus.py b/menus.py index 8e542e1..dbe8571 100644 --- a/menus.py +++ b/menus.py @@ -7,6 +7,8 @@ from dataobjs import pesterQuirk, PesterProfile from memos import TimeSlider, TimeInput from version import _pcVersion +import parsetools + _datadir = ostools.getDataDir() class PesterQuirkItem(QtGui.QTreeWidgetItem): @@ -245,28 +247,9 @@ class QuirkTesterWindow(QtGui.QDialog): @QtCore.pyqtSlot() def sentMessage(self): text = unicode(self.textInput.text()) - if text == "" or text[0:11] == "PESTERCHUM:": - return - self.history.add(text) - quirks = pesterQuirks(self.parent().testquirks()) - lexmsg = lexMessage(text) - if type(lexmsg[0]) is not mecmd: - try: - lexmsg = quirks.apply(lexmsg) - except Exception, e: - msgbox = QtGui.QMessageBox() - msgbox.setText("Whoa there! There seems to be a problem.") - msgbox.setInformativeText("A quirk seems to be having a problem. (Possibly you're trying to capture a non-existant group?)\n\ - %s" % e) - msgbox.exec_() - return - lexmsgs = splitMessage(lexmsg) - for lm in lexmsgs: - serverMsg = copy(lm) - self.addMessage(lm, True) - text = convertTags(serverMsg, "ctag") - self.textInput.setText("") + return parsetools.kxhandleInput(self, text, "menus") + def addMessage(self, msg, me=True): if type(msg) in [str, unicode]: lexmsg = lexMessage(msg) diff --git a/parsetools.py b/parsetools.py index 65d1a0a..ac0775b 100644 --- a/parsetools.py +++ b/parsetools.py @@ -1,15 +1,21 @@ import re import random import ostools +import collections from copy import copy from datetime import timedelta -from PyQt4 import QtGui +from PyQt4 import QtGui, QtCore from generic import mysteryTime from quirks import ScriptQuirks from pyquirks import PythonQuirks from luaquirks import LuaQuirks +# karxi: My own contribution to this - a proper lexer. +import pnc.lexercon as lexercon + +# I'll clean up the things that are no longer needed once the transition is +# actually finished. _ctag_begin = re.compile(r'(?i)') _gtag_begin = re.compile(r'(?i)') _ctag_end = re.compile(r'(?i)') @@ -61,7 +67,10 @@ def lexer(string, objlist): stringlist = copy(newstringlist) return stringlist -class colorBegin(object): +# karxi: All of these were derived from object before. I changed them to +# lexercon.Chunk so that I'd have an easier way to match against them until +# they're redone/removed. +class colorBegin(lexercon.Chunk): def __init__(self, string, color): self.string = string self.color = color @@ -87,7 +96,8 @@ class colorBegin(object): elif format == "ctag": (r,g,b,a) = qc.getRgb() return '' % (r,g,b) -class colorEnd(object): + +class colorEnd(lexercon.Chunk): def __init__(self, string): self.string = string def convert(self, format): @@ -99,7 +109,8 @@ class colorEnd(object): return "" else: return self.string -class formatBegin(object): + +class formatBegin(lexercon.Chunk): def __init__(self, string, ftype): self.string = string self.ftype = ftype @@ -112,7 +123,8 @@ class formatBegin(object): return "" else: return self.string -class formatEnd(object): + +class formatEnd(lexercon.Chunk): def __init__(self, string, ftype): self.string = string self.ftype = ftype @@ -125,7 +137,8 @@ class formatEnd(object): return "" else: return self.string -class hyperlink(object): + +class hyperlink(lexercon.Chunk): def __init__(self, string): self.string = string def convert(self, format): @@ -135,10 +148,12 @@ class hyperlink(object): return "[url]%s[/url]" % (self.string) else: return self.string + class hyperlink_lazy(hyperlink): def __init__(self, string): self.string = "http://" + string -class imagelink(object): + +class imagelink(lexercon.Chunk): def __init__(self, string, img): self.string = string self.img = img @@ -152,7 +167,8 @@ class imagelink(object): return "" else: return "" -class memolex(object): + +class memolex(lexercon.Chunk): def __init__(self, string, space, channel): self.string = string self.space = space @@ -162,7 +178,8 @@ class memolex(object): return "%s%s" % (self.space, self.channel, self.channel) else: return self.string -class chumhandlelex(object): + +class chumhandlelex(lexercon.Chunk): def __init__(self, string, space, handle): self.string = string self.space = space @@ -172,7 +189,8 @@ class chumhandlelex(object): return "%s%s" % (self.space, self.handle, self.handle) else: return self.string -class smiley(object): + +class smiley(lexercon.Chunk): def __init__(self, string): self.string = string def convert(self, format): @@ -180,7 +198,8 @@ class smiley(object): return "%s" % (smiledict[self.string], self.string, self.string) else: return self.string -class honker(object): + +class honker(lexercon.Chunk): def __init__(self, string): self.string = string def convert(self, format): @@ -188,13 +207,29 @@ class honker(object): return "" else: return self.string -class mecmd(object): + +class mecmd(lexercon.Chunk): def __init__(self, string, mecmd, suffix): self.string = string self.suffix = suffix def convert(self, format): return self.string +kxpclexer = lexercon.Pesterchum() + +def kxlexMsg(string): + # Do a bit of sanitization. + # TODO: Let people paste line-by-line normally. + msg = string.replace('\n', ' ').replace('\r', ' ') + # Something the original doesn't seem to have accounted for. + # Replace tabs with 4 spaces. + msg = msg.replace('\t', ' ' * 4) + msg = unicode(string) + # Begin lexing. + msg = kxpclexer.lex(msg) + # ...and that's it for this. + return msg + def lexMessage(string): lexlist = [(mecmd, _mecmdre), (colorBegin, _ctag_begin), (colorBegin, _gtag_begin), @@ -262,7 +297,7 @@ def _max_msg_len(mask=None, target=None): # Pesterchum. # Note that this effectively assumes the worst when not provided the # information it needs to get an accurate read, so later on, it'll need to - # be given a nick and the user's hostmask, as well as where the message is + # be given a nick or the user's hostmask, as well as where the message is # being sent. # It effectively has to construct the message that'll be sent in advance. limit = 512 @@ -299,46 +334,259 @@ def _max_msg_len(mask=None, target=None): return limit +def kxsplitMsg(lexed, fmt="pchum", maxlen=None, debug=False): + """Split messages so that they don't go over the length limit. + Returns a list of the messages, neatly split. + + Keep in mind that there's a little bit of magic involved in this at the + moment; some unsafe assumptions are made.""" + # Procedure: Lex. Convert for lengths as we go, keep starting tag + # length as we go too. Split whenever we hit the limit, add the tags to + # the start of the next line (or just keep a running line length + # total), and continue. + # N.B.: Keep the end tag length too. (+4 for each.) + # Copy the list so we can't break anything. + lexed = list(lexed) + working = [] + output = [] + open_ctags = [] + # Number of characters we've used. + curlen = 0 + # Maximum number of characters *to* use. + if not maxlen: + maxlen = _max_msg_len() + elif maxlen < 0: + # Subtract the (negative) length, giving us less leeway in this + # function. + maxlen = _max_msg_len() + maxlen + + # Defined here, but modified in the loop. + msglen = 0 + + def efflenleft(): + """Get the remaining space we have to work with, accounting for closing + tags that will be needed.""" + return maxlen - curlen - (len(open_ctags) * 4) + + safekeeping = lexed[:] + lexed = collections.deque(lexed) + rounds = 0 + while len(lexed) > 0: + rounds += 1 + if debug: + print "[Starting round {}...]".format(rounds) + msg = lexed.popleft() + msglen = 0 + is_text = False + text_preproc = False + + try: + msglen = len(msg.convert(fmt)) + except AttributeError: + # It's probably not a lexer tag. Assume a string. + # The input to this is supposed to be sanitary, after all. + msglen = len(msg) + # We allow this to error out if it fails for some reason. + # Remind us that it's a string, and thus can be split. + is_text = True + + # Test if we have room. + if msglen > efflenleft(): + # We do NOT have room - which means we need to think of how to + # handle this. + # If we have text, we can split it, keeping color codes in mind. + # Since those were already parsed, we don't have to worry about + # breaking something that way. + # Thus, we can split it, finalize it, and add the remainder to the + # next line (after the color codes). + if is_text and efflenleft() > 30: + text_preproc = True + # We use 30 as a general 'guess' - if there's less space than + # that, it's probably not worth trying to cram text in. + # This also saves us from infinitely trying to reduce the size + # of the input. + stack = [] + # We have text to split. + # This is okay because we don't apply the changes until the + # end - and the rest is shoved onto the stack to be dealt with + # immediately after. + lenl = efflenleft() + subround = 0 + while len(msg) > lenl: + subround += 1 + if debug: + print "[Splitting round {}-{}...]".format( + rounds, subround + ) + point = msg.rfind(' ', 0, lenl) + if point < 0: + # No spaces to break on...ugh. Break at the last space + # we can instead. + point = lenl ## - 1 + # NOTE: The - 1 is for safety (but probably isn't + # actually necessary.) + # Split and push what we have. + stack.append(msg[:point]) + # Remove what we just added. + msg = msg[point:] + if debug: + print "msg = {!r}".format(msg) + else: + # Catch the remainder. + stack.append(msg) + if debug: + print "msg caught; stack = {!r}".format(stack) + # Done processing. Pluck out the first portion so we can + # continue processing, then add the rest to our waiting list. + msg = stack.pop(0) + msglen = len(msg) + # Now we have a separated list, so we can add it. + # First we have to reverse it, because the extendleft method of + # deque objects - like our lexed queue - inserts the elements + # *backwards*.... + stack.reverse() + # Now we put them on 'top' of the proverbial deck, and deal + # with them next round. + lexed.extendleft(stack) + # We'll deal with those later. Now to get the 'msg' on the + # working list and finalize it for output - which really just + # means forcing the issue.... + working.append(msg) + curlen += msglen + + # Clear the slate. Add the remaining ctags, then add working to + # output, then clear working and statistics. Then we can move on to + # append as normal. + # Keep in mind that any open ctags get added to the beginning of + # working again, since they're still open! + + # ... + # ON SECOND THOUGHT: The lexer balances for us, so let's just use + # that for now. I can split up the function for this later. + working = ''.join(kxpclexer.list_convert(working)) + working = kxpclexer.lex(working) + working = ''.join(kxpclexer.list_convert(working)) + # TODO: Is that lazy? Yes. This is a modification made to test if + # it'll work, *not* if it'll be efficient. + + # Now that it's done the work for us, append and resume. + output.append(working) + # Reset working, starting it with the unclosed ctags. + working = open_ctags[:] + # Calculate the length of the starting tags, add it before anything + # else. + curlen = sum(len(tag.convert(fmt)) for tag in working) + if text_preproc: + # If we got here, it means we overflowed due to text - which + # means we also split and added it to working. There's no + # reason to continue and add it twice. + # This could be handled with an elif chain, but eh. + continue + # If we got here, it means we haven't done anything with 'msg' yet, + # in spite of popping it from lexed, so add it back for the next + # round. + # This sends it through for another round of splitting and work, + # possibly. + lexed.appendleft(msg) + continue + + # Normal tag processing stuff. Considerably less interesting/intensive + # than the text processing we did up there. + if isinstance(msg, lexercon.CTagEnd): + # Check for Ends first (subclassing issue). + if len(open_ctags) > 0: + # Don't add it unless it's going to make things /more/ even. + # We could have a Strict checker that errors on that, who + # knows. + # We just closed a ctag. + open_ctags.pop() + else: + # Ignore it. + # NOTE: I realize this is going to screw up something I do, but + # it also stops us from screwing up Chumdroid, so...whatever. + continue + elif isinstance(msg, lexercon.CTag): + # It's an opening color tag! + open_ctags.append(msg) + + # Add it to the working message. + working.append(msg) + + # Record the additional length. + # Also, need to be sure to account for the ends that would be added. + curlen += msglen + else: + # Once we're finally out of things to add, we're, well...out. + # So add working to the result one last time. + working = ''.join(kxpclexer.list_convert(working)) + output.append(working) + + # We're...done? + return output + def splitMessage(msg, format="ctag"): - """Splits message if it is too long.""" + """Splits message if it is too long. + This is the older version of this function, kept for compatibility. + It will eventually be phased out.""" # split long text lines buf = [] for o in msg: if type(o) in [str, unicode] and len(o) > 200: + # Split with a step of 200. I.e., cut long segments into chunks of + # 200 characters. + # I'm...not sure why this is done. I'll probably factor it out + # later on. for i in range(0, len(o), 200): buf.append(o[i:i+200]) else: + # Add non-text tags or 'short' segments without processing. buf.append(o) - msg = buf - okmsg = [] + # Copy the iterative variable. + msg = list(buf) + # This is the working segment. + working = [] + # Keep a stack of open color tags. cbegintags = [] + # This is the final result. output = [] + print repr(msg) for o in msg: oldctag = None - okmsg.append(o) + # Add to the working segment. + working.append(o) if type(o) is colorBegin: + # Track the open tag. cbegintags.append(o) elif type(o) is colorEnd: try: + # Remove the last open tag, since we've closed it. oldctag = cbegintags.pop() except IndexError: pass + # THIS part is the part I don't get. I'll revise it later.... + # It doesn't seem to catch ending ctags properly...or beginning ones. + # It's pretty much just broken, likely due to the line below. + # Maybe I can convert the tags, save the beginning tags, check their + # lengths and apply them after a split - or even iterate over each set, + # applying old tags before continuing...I don't know. # yeah normally i'd do binary search but im lazy - msglen = len(convertTags(okmsg, format)) + 4*(len(cbegintags)) + # Get length of beginning tags, and the end tags that'd be applied. + msglen = len(convertTags(working, format)) + 4*(len(cbegintags)) + # Previously this used 400. if msglen > _max_msg_len(): - okmsg.pop() + working.pop() if type(o) is colorBegin: cbegintags.pop() elif type(o) is colorEnd and oldctag is not None: cbegintags.append(oldctag) - if len(okmsg) == 0: + if len(working) == 0: output.append([o]) else: tmp = [] for color in cbegintags: - okmsg.append(colorEnd("")) + working.append(colorEnd("")) tmp.append(color) - output.append(okmsg) + output.append(working) if type(o) is colorBegin: cbegintags.append(o) elif type(o) is colorEnd: @@ -347,12 +595,183 @@ def splitMessage(msg, format="ctag"): except IndexError: pass tmp.append(o) - okmsg = tmp + working = tmp - if len(okmsg) > 0: - output.append(okmsg) + if len(working) > 0: + # Add any stragglers. + output.append(working) return output +def kxhandleInput(ctx, text=None, flavor=None): + """The function that user input that should be sent to the server is routed + through. Handles lexing, splitting, and quirk application.""" + # Flavor is important for logic, ctx is 'self'. + # Flavors are 'convo', 'menus', and 'memos' - so named after the source + # files for the original sentMessage variants. + + if flavor is None: + return ValueError("A flavor is needed to determine suitable logic!") + + if text is None: + # Fetch the raw text from the input box. + text = ctx.textInput.text() + text = unicode(ctx.textInput.text()) + + # Preprocessing stuff. + if text == "" or text.startswith("PESTERCHUM:"): + # We don't allow users to send system messages. There's also no + # point if they haven't entered anything. + # TODO: Consider accounting for the single-space bug, 'beloved' + # though it is. + return + + # Add the *raw* text to our history. + ctx.history.add(text) + + if flavor != "menus": + # Check if the line is OOC. Note that Pesterchum *is* kind enough to strip + # trailing spaces for us, even in the older versions. + oocDetected = oocre.match(text.strip()) + is_ooc = ctx.ooc or oocDetected + if ctx.ooc and not oocDetected: + # If we're supposed to be OOC, apply it artificially. + text = "(( %s ))" % (text) + # Also, quirk stuff. + should_quirk = ctx.applyquirks + else: + # 'menus' means a quirk tester window, which doesn't have an OOC + # variable. + is_ooc = False + should_quirk = True + is_action = text.startswith("/me") + + # Begin message processing. + msg = text + # We use 'text' despite its lack of processing because it's simpler. + if should_quirk and not (is_action or is_ooc): + # Fetch the quirks we'll have to apply. + quirks = ctx.mainwindow.userprofile.quirks + try: + # Do quirk things. (Ugly, but it'll have to do for now.) + # TODO: Look into the quirk system, modify/redo it. + # Gotta encapsulate or we might parse the wrong way. + msg = quirks.apply([msg]) + except Exception as err: + # Tell the user we couldn't do quirk things. + # TODO: Include the actual error...and the quirk it came from? + msgbox = QtGui.QMessageBox() + msgbox.setText("Whoa there! There seems to be a problem.") + err_info = "A quirk seems to be having a problem. (Error: {!s})" + err_info = err_info.format(err) + msgbox.setInformativeText(err_info) + msgbox.exec_() + return + + # Debug output. + print msg + # karxi: We have a list...but I'm not sure if we ever get anything else, so + # best to play it safe. I may remove this during later refactoring. + if isinstance(msg, list): + for i, m in enumerate(msg): + if isinstance(m, lexercon.Chunk): + # NOTE: KLUDGE. Filters out old PChum objects. + # karxi: This only works because I went in and subtyped them to + # an object type I provided - just so I could pluck them out + # later. + msg[i] = m.convert(format="ctag") + msg = ''.join(msg) + + # Quirks have been applied. Lex the messages (finally). + msg = kxlexMsg(msg) + + # Remove coloring if this is a /me! + if is_action: + # Filter out formatting specifiers (just ctags, at the moment). + msg = filter( + lambda m: not isinstance(m, + (lexercon.CTag, lexercon.CTagEnd) + ), + msg + ) + # We'll also add /me to the beginning of any new messages, later. + + # Put what's necessary in before splitting. + # Fetch our time if we're producing this for a memo. + if flavor == "memos": + if ctx.time.getTime() == None: + ctx.sendtime() + grammar = ctx.time.getGrammar() + # Oh, great...there's a parsing error to work around. Times are added + # automatically when received, but not when added directly?... I'll + # have to unify that. + # TODO: Fix parsing disparity. + initials = ctx.mainwindow.profile().initials() + colorcmd = ctx.mainwindow.profile().colorcmd() + # We'll use those later. + + # Split the messages so we don't go over the buffer and lose text. + maxlen = _max_msg_len() + # Since we have to do some post-processing, we need to adjust the maximum + # length we can use. + if flavor == "convo": + # The old Pesterchum setup used 200 for this. + maxlen = 300 + elif flavor == "memos": + # Use the max, with some room added so we can make additions. + maxlen -= 20 + + # Split the message. (Finally.) + # This is also set up to parse it into strings. + lexmsgs = kxsplitMsg(msg, "pchum", maxlen=maxlen) + # Strip off the excess. + for i, m in enumerate(lexmsgs): + lexmsgs[i] = m.strip() + + # Pester message handling. + if flavor == "convo": + # if ceased, rebegin + if hasattr(ctx, 'chumopen') and not ctx.chumopen: + ctx.mainwindow.newConvoStarted.emit( + QtCore.QString(ctx.title()), True + ) + ctx.setChumOpen(True) + + # Post-process and send the messages. + for i, lm in enumerate(lexmsgs): + # If we're working with an action and we split, it should have /mes. + if is_action and i > 0: + # Add them post-split. + lm = u"/me " + lm + # NOTE: No reason to reassign for now, but...why not? + lexmsgs[i] = lm + + # Copy the lexed result. + # Note that memos have to separate processing here. The adds and sends + # should be kept to the end because of that, near the emission. + clientMsg = copy(lm) + serverMsg = copy(lm) + + # Memo-specific processing. + if flavor == "memos" and not (is_action or is_ooc): + # Quirks were already applied, so get the prefix/postfix stuff + # ready. + # We fetched the information outside of the loop, so just + # construct the messages. + + clientMsg = u"{2}{3}{4}: {0}".format( + clientMsg, colorcmd, grammar.pcf, initials, grammar.number + ) + # Not sure if this needs a space at the end...? + serverMsg = u"{2}: {0}".format( + serverMsg, colorcmd, initials) + + ctx.addMessage(clientMsg, True) + if flavor != "menus": + # If we're not doing quirk testing, actually send. + ctx.messageSent.emit(serverMsg, ctx.title()) + + # Clear the input. + ctx.textInput.setText("") def addTimeInitial(string, grammar): @@ -426,6 +845,7 @@ class parseLeaf(object): out += n out = self.function(out) return out + class backreference(object): def __init__(self, number): self.number = number diff --git a/pnc/__init__.py b/pnc/__init__.py new file mode 100644 index 0000000..0085223 --- /dev/null +++ b/pnc/__init__.py @@ -0,0 +1,8 @@ +# -*- coding=UTF-8; tab-width: 4 -*- + +# These both seem to be worthless because they don't propogate to the modules +# lower down.... +##from __future__ import division +##from __future__ import absolute_import # JUST in case. + +# No __all__ or similar, for now. \ No newline at end of file diff --git a/pnc/dep/__init__.py b/pnc/dep/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pnc/dep/attrdict.py b/pnc/dep/attrdict.py new file mode 100644 index 0000000..c7eb74d --- /dev/null +++ b/pnc/dep/attrdict.py @@ -0,0 +1,49 @@ +# Modified version of the code featured at the given link +## {{{ http://code.activestate.com/recipes/473786/ (r1) +class AttrDict(dict): + """A dictionary with attribute-style access. It maps attribute access to + the real dictionary.""" + def __init__(self, init={}): super(AttrDict, self).__init__(init) + def __getstate__(self): return self.__dict__.items() + def __setstate__(self, items): + for key, val in items: self.__dict__[key] = val + def __repr__(self): + return "%s(%s)" % ( + type(self).__name__, + super(AttrDict, self).__repr__() + ) + def __setitem__(self, key, value): + return super(AttrDict, self).__setitem__(key, value) + def __getitem__(self, name): + return super(AttrDict, self).__getitem__(name) + def __delitem__(self, name): + return super(AttrDict, self).__delitem__(name) + __getattr__ = __getitem__ + __setattr__ = __setitem__ + __delattr__ = __delitem__ + def copy(self): return type(self)(self) +## end of http://code.activestate.com/recipes/473786/ }}} + +class DefAttrDict(AttrDict): + def __init__(self, default_factory=None, *args, **kwargs): + self.__dict__["default_factory"] = default_factory + super(DefAttrDict, self).__init__(*args, **kwargs) + def __repr__(self): + return "%s(%r, %s)" % ( + type(self).__name__, + self.default_factory, + super(AttrDict, self).__repr__() + ) + def __getitem__(self, name): + try: + return super(DefAttrDict, self).__getitem__(name) + except KeyError: + ##if self.default_factory is None: return None + ##return self.default_factory() + result = None + if self.default_factory is not None: + result = self.default_factory() + self[name] = result + return result + __getattr__ = __getitem__ + def copy(self): return type(self)(self.default_factory, self) \ No newline at end of file diff --git a/pnc/lexercon.py b/pnc/lexercon.py new file mode 100644 index 0000000..809499a --- /dev/null +++ b/pnc/lexercon.py @@ -0,0 +1,579 @@ +# -*- coding=UTF-8; tab-width: 4 -*- +from __future__ import division + +from .unicolor import Color + +import re + +global basestr +basestr = str +try: + basestr = basestring +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): + self.string = string + self.origin = origin + def __str__(self): + return self.string + def __len__(self): + return len(self.string) + def convert(self, format): + ##return self.string + # 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 + +# 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: + text = "" % color.to_rgb_tuple() + 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 = 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 + 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*)") + + 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 + 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 + + 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 + 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. + 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 diff --git a/pnc/unicolor.py b/pnc/unicolor.py new file mode 100644 index 0000000..da29939 --- /dev/null +++ b/pnc/unicolor.py @@ -0,0 +1,601 @@ +# -*- coding=UTF-8; tab-width: 4 -*- +from __future__ import division + +__all__ = ["Color"] + +# Copied from my old Textsub script. Please forgive the mess, and keep in mind +# that this may be phased out in the future. + + + +from .dep.attrdict import AttrDict + +import collections +import functools +import sys + +# Python 3 checking +if sys.version_info[0] == 2: + basestr = basestring +else: + basestr = str + + + +# A named tuple for containing CIE L*a*b* (CIELAB) information. +# NOTE TO THOSE MAINTAINING: If you don't know what that means, you're going to +# hate yourself *and* me if you try to edit this. I know I did when I wrote it. +LabTuple = collections.namedtuple("LabTuple", ['L', 'a', 'b']) +class Color(object): + # The threshold at which to consider two colors noticeably different, even + # if only barely + jnd = 2.3 + # TODO: Either subclass (this is probably best) or add a .native_type; in + # the case of the former, just make sure each type is geared towards using + # a certain kind of color space as a starting point, e.g. RGB, XYZ, HSV, + # CIELAB, etc... + # TODO: color_for_name() + # TODO: Split __init__, partly using __new__, so the former just has to do + # conversions + ##def __new__(cls, *args, **kwargs): + ## nargs = len(args) + ## if nargs > 0: arg = args[0] + ## if (nargs == 1 + ## and isinstance(arg, basestr) and not arg.startswith('#') + ## ): + ## # Try to look up the color name + ## name = arg.lower() + ## try: + ## color = _svg_colors[name] + ## except LookupError: + ## # We don't have a color with that name + ## raise ValueError("No color with name '%s' found" % name) + ## else: + ## # Hand over a copy of the color we found + ## return cls(color) + ## else: + ## return super(Color, cls).__new__(cls) + ## inst = super(Color, cls).__new__(cls) + ## inst.ccode = '' + ## nargs = len(args) + def __init__(self, *args, **kwargs): + self.ccode = '' + self.closest_name = self.name = None + nargs = len(args) + if nargs == 1: + # Make this a little easier to type out by reducing what we need to + # work with + arg = args[0] + if isinstance(arg, int): + # Assume we were passed a raw hexadecimal value + # Again, handle this the easy way + arg = "#%06X" % arg + # Using 'if' instead of 'elif' here allows us to fall through from + # the above, which is, of course, quite useful in this situation + if isinstance(arg, basestr): + # If it's a string, we've probably got a hex code, but check + # anyway just in case + if arg.startswith('#'): + self.hexstr = self.sanitize_hex(arg) + rgb = self.hexstr_to_rgb(self.hexstr) + self.red, self.green, self.blue = rgb + ##return + # TODO: This. + elif (arg.startswith('\003') and len(arg) > 1 + or len(arg) < 3 and arg.isdigit()): + # We have an IRC-style color code + arg = arg.lstrip('\003') + # Just in case + arg = arg.split(',')[0] + cnum = int(arg) + try: color = _irc_colors[cnum] + except LookupError: + raise ValueError("No color for ccode %r found" % cnum) + # We found a color; fall through and so on + arg = color + else: + # Presumably we have a color name + name = arg.lower() + try: color = _svg_colors[name] + except LookupError: + raise ValueError("No color with name %r found" % name) + # We found a color; fall through so we make this one a copy + arg = color + if isinstance(arg, Color): + # We were passed a Color object - just duplicate it. + # For now, we'll do things the cheap way.... + self.red, self.green, self.blue = arg.to_rgb_tuple() + self.hexstr = arg.hexstr + self.closest_name = arg.closest_name + self.name = arg.name + self.ccode = arg.ccode + elif nargs == 3: + # Assume we've got RGB + # Map to abs() so we can't get messed up results due to negatives + args = list(map(abs, args)) + self.red, self.green, self.blue = args + # Convert for the hex code + self.hexstr = self.rgb_to_hexstr(*args) + ##return + else: + # We don't know how to handle the value we recieved.... + raise ValueError + # Otherwise, calculate XYZ for this + self.x, self.y, self.z = self.rgb_to_xyz(*self.to_rgb_tuple()) + # Calculate the LAB color + self.cielab = LabTuple(*self.xyz_to_cielab(*self.to_xyz_tuple())) + if not self.closest_name: self.closest_name = self.get_svg_name() + if not self.ccode: self.ccode = self.get_ccode() + + def __eq__(self, other): + return hash(self) == hash(other) + def __ne__(self, other): return not self.__eq__(other) + def __sub__(self, other): + if not isinstance(other, Color): raise TypeError + return self.distance(other) + + def __hash__(self): + ##result = map(hash, [ + ## str(self).upper(), + ## self.red, self.green, self.blue + ## ]) + # 2012-12-08T13:34-07:00: This should improve accuracy + result = map(hash, self.cielab) + result = functools.reduce(lambda x, y: x ^ y, result) + return result + + # Before the change on 2012-12-08, the above was equivalent to the old + # code, which was this: + ##result = hash(str(self).upper()) + ##result ^= self.red + ##result ^= self.green + ##result ^= self.blue + ##return result + def __repr__(self): + ##return "%s(%r)" % (type(self).__name__, str(self)) + return "%s(%r)" % (type(self).__name__, + self.reduce_hexstr(self.hexstr)) + def __str__(self): + ##return self.reduce_hexstr(self.hexstr) + return self.name() + + # Builtins + # These were yanked from Hostmask and changed around a bit + def __getitem__(self, ind): return (self.red, self.green, self.blue)[ind] + def __iter__(self): + targs = (self.red, self.green, self.blue) + for t in targs: + yield t + # If we got here, we're out of attributes to provide + raise StopIteration + ##def __len__(self): + ## # Acceptable so long as we're returning RGB like we currently (at TOW) + ## # are + ## return 3 + + @classmethod + def from_ccode(cls, ccode): + if isinstance(ccode, basestr): + # We were passed a string + ccode = ccode.lstrip('\003') + ccode = ccode.split(',') + if len(ccode) < 2: + fg = ccode[0] + bg = None + else: + fg, bg = ccode + try: + fg = int(fg) + except ValueError: + # We started with a string to the effect of ",00" + fg = -1 + try: + bg = int(bg) + except (ValueError, TypeError): + # We started with a string to the effect of "00,", or it didn't + # have a comma + bg = -1 + else: + fg = ccode + bg = -1 + + try: + fg = _irc_colors[fg] + except LookupError: + # We had a string to the effect of ",00", or the color code + # couldn't be found + # TODO: Consider making a ValueError return a different value? + fg = None + else: + fg = Color(fg) + try: + bg = _irc_colors[bg] + except LookupError: + # See above note. + bg = None + else: + bg = Color(bg) + ##if bg: return fg, bg + return fg, bg + + def get_ccode(self): + closest, cldist = None, None + targs = _irc_colors + + for code, other in targs.items(): + dist = self - other + ##if (not strict and dist > self.jnd) or dist == 0: + if dist == 0: + # We have a perfect match! + # Just return the relevant color code right now + return "%02d" % code + if cldist is None or cldist > dist: + closest, cldist = "%02d" % code, dist + # We've found the closest matching color code; return it + return closest + + def get_svg_name(self, strict=False): + closest, cldist = None, None + targs = _svg_colors + + for name, other in targs.items(): + dist = self - other + if (not strict and dist > self.jnd) or dist == 0: + # The difference is below the Just-Noticeable Difference + # threshold, or we have a perfect match; consider them roughly + # the same + return name + if cldist is None or cldist > dist: + closest, cldist = name, dist + # We've found the closest matching color name; return it + return closest + + ##def name(self): return self.closest_name + + def distance(self, other): + # CIELAB distance, adapted from distance() and checked vs. Wikipedia: + # http://en.wikipedia.org/wiki/Color_difference + slab, olab = self.to_cielab_tuple(), other.to_cielab_tuple() + # Calculate the distance between the points for each + dist = map(lambda p1, p2: (p2 - p1)**2, slab, olab) + # Add the results up, and sqrt to compensate for earlier squaring + dist = sum(dist) ** .5 + return dist + + def rgb_distance(self, other): + # The older version of distance(). + ##r1, r2 = self.red, other.red + ##g1, g2 = self.green, other.green + ##b1, b2 = self.blue, other.blue + srgb, orgb = self.to_rgb_tuple(), other.to_rgb_tuple() + ### Subtract the RGBs from each other (i.e. r1 - r2, g1 - g2, b1 - b2) + ##dist = map(operator.sub, srgb, orgb) + ### Square the results from the above + ##dist = [x**2 for x in dist] + # Do what we WOULD have done in those two lines with a single one + dist = map(lambda x1, x2: (x1 - x2)**2, srgb, orgb) + # Add the results up + dist = sum(dist) + # Fetch the square root to compensate for the earlier squaring + dist **= .5 + return dist + + @classmethod + def hexstr_to_rgb(cls, hexstr): + hexstr = cls.sanitize_hex(hexstr) + hexstr = hexstr.lstrip('#') + # This is ugly, but the purpose is simple and it's accomplished in a + # single line...it just runs through the string, picking two characters + # at a time and converting them from hex values to ints. + result = tuple(int(hexstr[i:i+2], 16) for i in range(len(hexstr))[::2]) + return result + ##working = collections.deque(hexstr) + ##result = [] + ### The alternative to doing it this way would be to use an int-driven + ### 'for' loop, or similar which might be preferable + ##while working: + ## # Fetch the next two args and convert them to an int + ## i = int(working.popleft() + working.popleft(), 16) + ## result.append(i) + + + @staticmethod + def rgb_to_hexstr(red, green, blue): + rgb = [red, green, blue] + rgb = map(abs, rgb) + # Preemptively add the '#' to our result + result = ['#'] + for c in rgb: + ### Convert to hex, stripping the leading "0x" that Python adds + ##c = hex(c).lstrip("0x", 1) + ### Add a '0' in front if it's just a single digit + ##c = ('0' + c)[-2:] + c = "%02X" % c + # Append to our result + result.append(c) + # Join and return the result + return ''.join(result) + + # These next two are from http://www.easyrgb.com/index.php?X=MATH + @staticmethod + def rgb_to_xyz(red, green, blue): + rgb = [red, green, blue] + for i, n in enumerate(rgb): + n /= 255 + if n > 0.04045: n = ( ( n + 0.055 ) / 1.055 ) ** 2.4 + else: n /= 12.92 + rgb[i] = n * 100 + r, g, b = rgb + x = r * 0.4124 + g * 0.3576 + b * 0.1805 + y = r * 0.2126 + g * 0.7152 + b * 0.0722 + z = r * 0.0193 + g * 0.1192 + b * 0.9505 + ##x = 0.436052025 * r + 0.385081593 * g + 0.143087414 * b + ##y = 0.222491598 * r + 0.71688606 * g + 0.060621486 * b + ##z = 0.013929122 * r + 0.097097002 * g + 0.71418547 * b + return x, y, z + @staticmethod + def xyz_to_cielab(x, y, z): + # Reference X, Y, and Z + refs = [95.047, 100.000, 108.883] + ref_x, ref_y, ref_z = refs + ##xyz = [x / ref_x, y / ref_y, z / ref_z] + xyz = [x, y, z] + for i, n in enumerate(xyz): + n /= refs[i] + if n > 0.008856: n **= 1/3 + else: + n *= 7.787 + n += 16/116 + xyz[i] = n + x, y, z = xyz + l = (y*116) - 16 + a = (x - y) * 500 + b = (y - z) * 200 + return l, a, b + + @staticmethod + def reduce_hexstr(hexstr): + orig = hexstr + hexstr = hexstr.lstrip('#') + lhexstr = hexstr.lower() + strlen = len(hexstr) + working = ['#'] + for i in range(strlen)[::2]: + if lhexstr[i] == lhexstr[i+1]: + # The two characters fetched are the same, so we can reduce + working.append(hexstr[i]) + else: + # The two characters differ, so we can't actually reduce this + # string at all; just return the original + return orig + # If we got here, we successfully reduced + return ''.join(working) + + + @staticmethod + def sanitize_hex(hexstr): + hexstr = hexstr.upper() + # We don't need the leading hash mark for now + hexstr = hexstr.lstrip('#') + strlen = len(hexstr) + if strlen == 6: + return '#' + hexstr + elif strlen == 3: + # We have a short (CSS style) code; duplicate all of the characters + hexstr = [c + c for c in hexstr] + hexstr = ''.join(hexstr) + return '#' + hexstr + # Should probably error out, but that can wait until later + return '#' + hexstr + + def to_cielab_tuple(self): + # For now, just return the stored CIELAB tuple + return self.cielab + def to_rgb_tuple(self): return (self.red, self.green, self.blue) + # 2012-12-05T17:40:39-07:00: Changed 'self.blue' to 'self.z' like it SHOULD + # have been in the FIRST place. Ugh. How did I fuck THAT one up? + def to_xyz_tuple(self): return (self.x, self.y, self.z) + +# All of these are effectively equivalent to the Qt-provided colors, so they +# could be phased out - but there's no need to, yet. +_svg_colors = AttrDict() +_irc_colors = {} +_svg_colors.update({ + "aliceblue": Color(240, 248, 255), + "antiquewhite": Color(250, 235, 215), + "aqua": Color( 0, 255, 255), + "aquamarine": Color(127, 255, 212), + "azure": Color(240, 255, 255), + "beige": Color(245, 245, 220), + "bisque": Color(255, 228, 196), + "black": Color( 0, 0, 0), + "blanchedalmond": Color(255, 235, 205), + "blue": Color( 0, 0, 255), + "blueviolet": Color(138, 43, 226), + "brown": Color(165, 42, 42), + "burlywood": Color(222, 184, 135), + "cadetblue": Color( 95, 158, 160), + "chartreuse": Color(127, 255, 0), + "chocolate": Color(210, 105, 30), + "coral": Color(255, 127, 80), + "cornflowerblue": Color(100, 149, 237), + "cornsilk": Color(255, 248, 220), + "crimson": Color(220, 20, 60), + "cyan": Color( 0, 255, 255), + "darkblue": Color( 0, 0, 139), + "darkcyan": Color( 0, 139, 139), + "darkgoldenrod": Color(184, 134, 11), + "darkgray": Color(169, 169, 169), + "darkgreen": Color( 0, 100, 0), + "darkgrey": Color(169, 169, 169), + "darkkhaki": Color(189, 183, 107), + "darkmagenta": Color(139, 0, 139), + "darkolivegreen": Color( 85, 107, 47), + "darkorange": Color(255, 140, 0), + "darkorchid": Color(153, 50, 204), + "darkred": Color(139, 0, 0), + "darksalmon": Color(233, 150, 122), + "darkseagreen": Color(143, 188, 143), + "darkslateblue": Color( 72, 61, 139), + "darkslategray": Color( 47, 79, 79), + "darkslategrey": Color( 47, 79, 79), + "darkturquoise": Color( 0, 206, 209), + "darkviolet": Color(148, 0, 211), + "deeppink": Color(255, 20, 147), + "deepskyblue": Color( 0, 191, 255), + "dimgray": Color(105, 105, 105), + "dimgrey": Color(105, 105, 105), + "dodgerblue": Color( 30, 144, 255), + "firebrick": Color(178, 34, 34), + "floralwhite": Color(255, 250, 240), + "forestgreen": Color( 34, 139, 34), + "fuchsia": Color(255, 0, 255), + "gainsboro": Color(220, 220, 220), + "ghostwhite": Color(248, 248, 255), + "gold": Color(255, 215, 0), + "goldenrod": Color(218, 165, 32), + "gray": Color(128, 128, 128), + "grey": Color(128, 128, 128), + "green": Color( 0, 128, 0), + "greenyellow": Color(173, 255, 47), + "honeydew": Color(240, 255, 240), + "hotpink": Color(255, 105, 180), + "indianred": Color(205, 92, 92), + "indigo": Color( 75, 0, 130), + "ivory": Color(255, 255, 240), + "khaki": Color(240, 230, 140), + "lavender": Color(230, 230, 250), + "lavenderblush": Color(255, 240, 245), + "lawngreen": Color(124, 252, 0), + "lemonchiffon": Color(255, 250, 205), + "lightblue": Color(173, 216, 230), + "lightcoral": Color(240, 128, 128), + "lightcyan": Color(224, 255, 255), + "lightgoldenrodyellow": Color(250, 250, 210), + "lightgray": Color(211, 211, 211), + "lightgreen": Color(144, 238, 144), + "lightgrey": Color(211, 211, 211), + "lightpink": Color(255, 182, 193), + "lightsalmon": Color(255, 160, 122), + "lightseagreen": Color( 32, 178, 170), + "lightskyblue": Color(135, 206, 250), + "lightslategray": Color(119, 136, 153), + "lightslategrey": Color(119, 136, 153), + "lightsteelblue": Color(176, 196, 222), + "lightyellow": Color(255, 255, 224), + "lime": Color( 0, 255, 0), + "limegreen": Color( 50, 205, 50), + "linen": Color(250, 240, 230), + "magenta": Color(255, 0, 255), + "maroon": Color(128, 0, 0), + "mediumaquamarine": Color(102, 205, 170), + "mediumblue": Color( 0, 0, 205), + "mediumorchid": Color(186, 85, 211), + "mediumpurple": Color(147, 112, 219), + "mediumseagreen": Color( 60, 179, 113), + "mediumslateblue": Color(123, 104, 238), + "mediumspringgreen": Color( 0, 250, 154), + "mediumturquoise": Color( 72, 209, 204), + "mediumvioletred": Color(199, 21, 133), + "midnightblue": Color( 25, 25, 112), + "mintcream": Color(245, 255, 250), + "mistyrose": Color(255, 228, 225), + "moccasin": Color(255, 228, 181), + "navajowhite": Color(255, 222, 173), + "navy": Color( 0, 0, 128), + "oldlace": Color(253, 245, 230), + "olive": Color(128, 128, 0), + "olivedrab": Color(107, 142, 35), + "orange": Color(255, 165, 0), + "orangered": Color(255, 69, 0), + "orchid": Color(218, 112, 214), + "palegoldenrod": Color(238, 232, 170), + "palegreen": Color(152, 251, 152), + "paleturquoise": Color(175, 238, 238), + "palevioletred": Color(219, 112, 147), + "papayawhip": Color(255, 239, 213), + "peachpuff": Color(255, 218, 185), + "peru": Color(205, 133, 63), + "pink": Color(255, 192, 203), + "plum": Color(221, 160, 221), + "powderblue": Color(176, 224, 230), + "purple": Color(128, 0, 128), + "red": Color(255, 0, 0), + "rosybrown": Color(188, 143, 143), + "royalblue": Color( 65, 105, 225), + "saddlebrown": Color(139, 69, 19), + "salmon": Color(250, 128, 114), + "sandybrown": Color(244, 164, 96), + "seagreen": Color( 46, 139, 87), + "seashell": Color(255, 245, 238), + "sienna": Color(160, 82, 45), + "silver": Color(192, 192, 192), + "skyblue": Color(135, 206, 235), + "slateblue": Color(106, 90, 205), + "slategray": Color(112, 128, 144), + "slategrey": Color(112, 128, 144), + "snow": Color(255, 250, 250), + "springgreen": Color( 0, 255, 127), + "steelblue": Color( 70, 130, 180), + "tan": Color(210, 180, 140), + "teal": Color( 0, 128, 128), + "thistle": Color(216, 191, 216), + "tomato": Color(255, 99, 71), + "turquoise": Color( 64, 224, 208), + "violet": Color(238, 130, 238), + "wheat": Color(245, 222, 179), + "white": Color(255, 255, 255), + "whitesmoke": Color(245, 245, 245), + "yellow": Color(255, 255, 0), + "yellowgreen": Color(154, 205, 50) + }) +for k, v in _svg_colors.items(): + v.closest_name = v.name = k + +# 2012-12-08T14:29-07:00: Copied over from Colors.hexstr_for_ccodes in the main +# textsub file, and subsequently modified. +_irc_colors.update({ + # These are all taken from *MY* XChat settings - they aren't guaranteed to + # please everyone! + 0: Color(0xFFFFFF), + 1: Color(0x1F1F1F), + 2: Color(0x00007F), + 3: Color(0x007F00), + 4: Color(0xFF0000), + 5: Color(0x7F0000), + 6: Color(0x9C009C), + 7: Color(0xFC7F00), + 8: Color(0xFFFF00), + 9: Color(0x00FC00), + ##10: Color(0x009393), + 10: Color(0x008282), + 11: Color(0x00FFFF), + 12: Color(0x0000FC), + 13: Color(0xFF00FF), + 14: Color(0x7F7F7F), + 15: Color(0xD2D2D2), + # My local colors + 16: Color(0xCCCCCC), + ##17: Color(0x000000), # Commented out 'til readability checks are in + 17: Color(0x1F1F1F), + 18: Color(0x000056), + 19: Color(0x008141), + 20: Color(0xE00707), + 21: Color(0xA10000), + 22: Color(0x6A006A), + 23: Color(0xA15000), + 24: Color(0xA1A100), + 25: Color(0x416600), + ##26: Color(0x008282), + 26: Color(0x005682), + 27: Color(0x00D5F2), + 28: Color(0x0715CD), + 29: Color(0x99004D), + 30: Color(0x323232), + 31: Color(0x929292), + + 99: Color(0x999999) # Until I think of a better solution to this + }) +for k, v in _irc_colors.items(): + v.ccode = "%02d" % k +del k, v