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 "" % (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