Merged in some new lexer code. Older code will be phased out over time.

This code should split things more neatly than the current Pesterchum
code, thus fixing a number of irritating bugs. Ideally, when finished,
it will be easier and cleaner to work with as well.
This commit is contained in:
karxi 2016-11-18 03:37:22 -05:00
parent 6a34f769ed
commit 876e06f217
9 changed files with 1697 additions and 104 deletions

View file

@ -12,6 +12,9 @@ from dataobjs import PesterProfile, PesterHistory
from generic import PesterIcon from generic import PesterIcon
from parsetools import convertTags, lexMessage, splitMessage, mecmd, colorBegin, colorEnd, \ from parsetools import convertTags, lexMessage, splitMessage, mecmd, colorBegin, colorEnd, \
img2smiley, smiledict, oocre img2smiley, smiledict, oocre
import parsetools
import pnc.lexercon as lexercon
class PesterTabWindow(QtGui.QFrame): class PesterTabWindow(QtGui.QFrame):
def __init__(self, mainwindow, parent=None, convo="convo"): def __init__(self, mainwindow, parent=None, convo="convo"):
@ -721,36 +724,12 @@ class PesterConvo(QtGui.QFrame):
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def sentMessage(self): 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()) 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: return parsetools.kxhandleInput(self, text, flavor="convo")
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("")
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def addThisChum(self): def addThisChum(self):

View file

@ -10,6 +10,7 @@ from generic import PesterIcon, RightClickList, mysteryTime
from convo import PesterConvo, PesterInput, PesterText, PesterTabWindow from convo import PesterConvo, PesterInput, PesterText, PesterTabWindow
from parsetools import convertTags, addTimeInitial, timeProtocol, \ from parsetools import convertTags, addTimeInitial, timeProtocol, \
lexMessage, colorBegin, colorEnd, mecmd, smiledict, oocre lexMessage, colorBegin, colorEnd, mecmd, smiledict, oocre
import parsetools
from logviewer import PesterLogViewer from logviewer import PesterLogViewer
def delta2txt(d, format="pc"): def delta2txt(d, format="pc"):
@ -805,36 +806,9 @@ class PesterMemo(PesterConvo):
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def sentMessage(self): def sentMessage(self):
text = unicode(self.textInput.text()) text = unicode(self.textInput.text())
if text == "" or text[0:11] == "PESTERCHUM:":
return return parsetools.kxhandleInput(self, text, flavor="memos")
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("<c=%s>" % (colorcmd), colorcmd),
"%s%s%s: " % (grammar.pcf, initials, grammar.number)] + lexmsg + [colorEnd("</c>")]
# account for TC's parsing error
serverMsg = [colorBegin("<c=%s>" % (colorcmd), colorcmd),
"%s: " % (initials)] + lexmsg + [colorEnd("</c>"), " "]
else:
clientMsg = copy(lexmsg)
serverMsg = copy(lexmsg)
self.addMessage(clientMsg, True)
serverText = convertTags(serverMsg, "ctag")
self.messageSent.emit(serverText, self.title())
self.textInput.setText("")
@QtCore.pyqtSlot(QtCore.QString) @QtCore.pyqtSlot(QtCore.QString)
def namesUpdated(self, channel): def namesUpdated(self, channel):
c = unicode(channel) c = unicode(channel)

View file

@ -7,6 +7,8 @@ from dataobjs import pesterQuirk, PesterProfile
from memos import TimeSlider, TimeInput from memos import TimeSlider, TimeInput
from version import _pcVersion from version import _pcVersion
import parsetools
_datadir = ostools.getDataDir() _datadir = ostools.getDataDir()
class PesterQuirkItem(QtGui.QTreeWidgetItem): class PesterQuirkItem(QtGui.QTreeWidgetItem):
@ -245,28 +247,9 @@ class QuirkTesterWindow(QtGui.QDialog):
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def sentMessage(self): def sentMessage(self):
text = unicode(self.textInput.text()) 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: return parsetools.kxhandleInput(self, text, "menus")
serverMsg = copy(lm)
self.addMessage(lm, True)
text = convertTags(serverMsg, "ctag")
self.textInput.setText("")
def addMessage(self, msg, me=True): def addMessage(self, msg, me=True):
if type(msg) in [str, unicode]: if type(msg) in [str, unicode]:
lexmsg = lexMessage(msg) lexmsg = lexMessage(msg)

View file

@ -1,15 +1,21 @@
import re import re
import random import random
import ostools import ostools
import collections
from copy import copy from copy import copy
from datetime import timedelta from datetime import timedelta
from PyQt4 import QtGui from PyQt4 import QtGui, QtCore
from generic import mysteryTime from generic import mysteryTime
from quirks import ScriptQuirks from quirks import ScriptQuirks
from pyquirks import PythonQuirks from pyquirks import PythonQuirks
from luaquirks import LuaQuirks 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)<c=(.*?)>') _ctag_begin = re.compile(r'(?i)<c=(.*?)>')
_gtag_begin = re.compile(r'(?i)<g[a-f]>') _gtag_begin = re.compile(r'(?i)<g[a-f]>')
_ctag_end = re.compile(r'(?i)</c>') _ctag_end = re.compile(r'(?i)</c>')
@ -61,7 +67,10 @@ def lexer(string, objlist):
stringlist = copy(newstringlist) stringlist = copy(newstringlist)
return stringlist 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): def __init__(self, string, color):
self.string = string self.string = string
self.color = color self.color = color
@ -87,7 +96,8 @@ class colorBegin(object):
elif format == "ctag": elif format == "ctag":
(r,g,b,a) = qc.getRgb() (r,g,b,a) = qc.getRgb()
return '<c=%s,%s,%s>' % (r,g,b) return '<c=%s,%s,%s>' % (r,g,b)
class colorEnd(object):
class colorEnd(lexercon.Chunk):
def __init__(self, string): def __init__(self, string):
self.string = string self.string = string
def convert(self, format): def convert(self, format):
@ -99,7 +109,8 @@ class colorEnd(object):
return "" return ""
else: else:
return self.string return self.string
class formatBegin(object):
class formatBegin(lexercon.Chunk):
def __init__(self, string, ftype): def __init__(self, string, ftype):
self.string = string self.string = string
self.ftype = ftype self.ftype = ftype
@ -112,7 +123,8 @@ class formatBegin(object):
return "" return ""
else: else:
return self.string return self.string
class formatEnd(object):
class formatEnd(lexercon.Chunk):
def __init__(self, string, ftype): def __init__(self, string, ftype):
self.string = string self.string = string
self.ftype = ftype self.ftype = ftype
@ -125,7 +137,8 @@ class formatEnd(object):
return "" return ""
else: else:
return self.string return self.string
class hyperlink(object):
class hyperlink(lexercon.Chunk):
def __init__(self, string): def __init__(self, string):
self.string = string self.string = string
def convert(self, format): def convert(self, format):
@ -135,10 +148,12 @@ class hyperlink(object):
return "[url]%s[/url]" % (self.string) return "[url]%s[/url]" % (self.string)
else: else:
return self.string return self.string
class hyperlink_lazy(hyperlink): class hyperlink_lazy(hyperlink):
def __init__(self, string): def __init__(self, string):
self.string = "http://" + string self.string = "http://" + string
class imagelink(object):
class imagelink(lexercon.Chunk):
def __init__(self, string, img): def __init__(self, string, img):
self.string = string self.string = string
self.img = img self.img = img
@ -152,7 +167,8 @@ class imagelink(object):
return "" return ""
else: else:
return "" return ""
class memolex(object):
class memolex(lexercon.Chunk):
def __init__(self, string, space, channel): def __init__(self, string, space, channel):
self.string = string self.string = string
self.space = space self.space = space
@ -162,7 +178,8 @@ class memolex(object):
return "%s<a href='%s'>%s</a>" % (self.space, self.channel, self.channel) return "%s<a href='%s'>%s</a>" % (self.space, self.channel, self.channel)
else: else:
return self.string return self.string
class chumhandlelex(object):
class chumhandlelex(lexercon.Chunk):
def __init__(self, string, space, handle): def __init__(self, string, space, handle):
self.string = string self.string = string
self.space = space self.space = space
@ -172,7 +189,8 @@ class chumhandlelex(object):
return "%s<a href='%s'>%s</a>" % (self.space, self.handle, self.handle) return "%s<a href='%s'>%s</a>" % (self.space, self.handle, self.handle)
else: else:
return self.string return self.string
class smiley(object):
class smiley(lexercon.Chunk):
def __init__(self, string): def __init__(self, string):
self.string = string self.string = string
def convert(self, format): def convert(self, format):
@ -180,7 +198,8 @@ class smiley(object):
return "<img src='smilies/%s' alt='%s' title='%s' />" % (smiledict[self.string], self.string, self.string) return "<img src='smilies/%s' alt='%s' title='%s' />" % (smiledict[self.string], self.string, self.string)
else: else:
return self.string return self.string
class honker(object):
class honker(lexercon.Chunk):
def __init__(self, string): def __init__(self, string):
self.string = string self.string = string
def convert(self, format): def convert(self, format):
@ -188,13 +207,29 @@ class honker(object):
return "<img src='smilies/honk.png' alt'honk' title='honk' />" return "<img src='smilies/honk.png' alt'honk' title='honk' />"
else: else:
return self.string return self.string
class mecmd(object):
class mecmd(lexercon.Chunk):
def __init__(self, string, mecmd, suffix): def __init__(self, string, mecmd, suffix):
self.string = string self.string = string
self.suffix = suffix self.suffix = suffix
def convert(self, format): def convert(self, format):
return self.string 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): def lexMessage(string):
lexlist = [(mecmd, _mecmdre), lexlist = [(mecmd, _mecmdre),
(colorBegin, _ctag_begin), (colorBegin, _gtag_begin), (colorBegin, _ctag_begin), (colorBegin, _gtag_begin),
@ -262,7 +297,7 @@ def _max_msg_len(mask=None, target=None):
# Pesterchum. # Pesterchum.
# Note that this effectively assumes the worst when not provided the # 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 # 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. # being sent.
# It effectively has to construct the message that'll be sent in advance. # It effectively has to construct the message that'll be sent in advance.
limit = 512 limit = 512
@ -299,46 +334,259 @@ def _max_msg_len(mask=None, target=None):
return limit 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"): 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 # split long text lines
buf = [] buf = []
for o in msg: for o in msg:
if type(o) in [str, unicode] and len(o) > 200: 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): for i in range(0, len(o), 200):
buf.append(o[i:i+200]) buf.append(o[i:i+200])
else: else:
# Add non-text tags or 'short' segments without processing.
buf.append(o) buf.append(o)
msg = buf # Copy the iterative variable.
okmsg = [] msg = list(buf)
# This is the working segment.
working = []
# Keep a stack of open color tags.
cbegintags = [] cbegintags = []
# This is the final result.
output = [] output = []
print repr(msg)
for o in msg: for o in msg:
oldctag = None oldctag = None
okmsg.append(o) # Add to the working segment.
working.append(o)
if type(o) is colorBegin: if type(o) is colorBegin:
# Track the open tag.
cbegintags.append(o) cbegintags.append(o)
elif type(o) is colorEnd: elif type(o) is colorEnd:
try: try:
# Remove the last open tag, since we've closed it.
oldctag = cbegintags.pop() oldctag = cbegintags.pop()
except IndexError: except IndexError:
pass 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 # 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(): if msglen > _max_msg_len():
okmsg.pop() working.pop()
if type(o) is colorBegin: if type(o) is colorBegin:
cbegintags.pop() cbegintags.pop()
elif type(o) is colorEnd and oldctag is not None: elif type(o) is colorEnd and oldctag is not None:
cbegintags.append(oldctag) cbegintags.append(oldctag)
if len(okmsg) == 0: if len(working) == 0:
output.append([o]) output.append([o])
else: else:
tmp = [] tmp = []
for color in cbegintags: for color in cbegintags:
okmsg.append(colorEnd("</c>")) working.append(colorEnd("</c>"))
tmp.append(color) tmp.append(color)
output.append(okmsg) output.append(working)
if type(o) is colorBegin: if type(o) is colorBegin:
cbegintags.append(o) cbegintags.append(o)
elif type(o) is colorEnd: elif type(o) is colorEnd:
@ -347,12 +595,183 @@ def splitMessage(msg, format="ctag"):
except IndexError: except IndexError:
pass pass
tmp.append(o) tmp.append(o)
okmsg = tmp working = tmp
if len(okmsg) > 0: if len(working) > 0:
output.append(okmsg) # Add any stragglers.
output.append(working)
return output 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"<c={1}>{2}{3}{4}: {0}</c>".format(
clientMsg, colorcmd, grammar.pcf, initials, grammar.number
)
# Not sure if this needs a space at the end...?
serverMsg = u"<c={1}>{2}: {0}</c>".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): def addTimeInitial(string, grammar):
@ -426,6 +845,7 @@ class parseLeaf(object):
out += n out += n
out = self.function(out) out = self.function(out)
return out return out
class backreference(object): class backreference(object):
def __init__(self, number): def __init__(self, number):
self.number = number self.number = number

8
pnc/__init__.py Normal file
View file

@ -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.

0
pnc/dep/__init__.py Normal file
View file

49
pnc/dep/attrdict.py Normal file
View file

@ -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)

579
pnc/lexercon.py Normal file
View file

@ -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 = "</c>"
else:
if color.name:
text = "<c=%s>" % color.name
else:
text = "<c=%d,%d,%d>" % 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 "</c>"
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 "</c>"
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"<c=(.*?)>", flags=re.I)
_ctag_rgb = re.compile(r"(\d+),(\d+),(\d+)")
_ctag_end = re.compile(r"</c>", 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("</c>"))
balanced.append(CTagEnd("</c>", 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("</c>", self.ref, None))
closecolor = lambda: converted.append(CTagEnd("</c>", 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 </c> 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 </c>
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<fg>\d\d?)?(?(fg),(?P<bg>\d\d?))?|\x0F")
_ccode_rxp = re.compile(r"\x03(?P<fg>\d\d?)(?(fg),(?P<bg>\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 </c> 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 </c>
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

601
pnc/unicolor.py Normal file
View file

@ -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