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:
parent
6a34f769ed
commit
876e06f217
9 changed files with 1697 additions and 104 deletions
35
convo.py
35
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):
|
||||
|
|
34
memos.py
34
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("<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("")
|
||||
|
||||
return parsetools.kxhandleInput(self, text, flavor="memos")
|
||||
|
||||
@QtCore.pyqtSlot(QtCore.QString)
|
||||
def namesUpdated(self, channel):
|
||||
c = unicode(channel)
|
||||
|
|
25
menus.py
25
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)
|
||||
|
|
470
parsetools.py
470
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)<c=(.*?)>')
|
||||
_gtag_begin = re.compile(r'(?i)<g[a-f]>')
|
||||
_ctag_end = re.compile(r'(?i)</c>')
|
||||
|
@ -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 '<c=%s,%s,%s>' % (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<a href='%s'>%s</a>" % (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<a href='%s'>%s</a>" % (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 "<img src='smilies/%s' alt='%s' title='%s' />" % (smiledict[self.string], self.string, self.string)
|
||||
else:
|
||||
return self.string
|
||||
class honker(object):
|
||||
|
||||
class honker(lexercon.Chunk):
|
||||
def __init__(self, string):
|
||||
self.string = string
|
||||
def convert(self, format):
|
||||
|
@ -188,13 +207,29 @@ class honker(object):
|
|||
return "<img src='smilies/honk.png' alt'honk' title='honk' />"
|
||||
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("</c>"))
|
||||
working.append(colorEnd("</c>"))
|
||||
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"<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):
|
||||
|
@ -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
|
||||
|
|
8
pnc/__init__.py
Normal file
8
pnc/__init__.py
Normal 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
0
pnc/dep/__init__.py
Normal file
49
pnc/dep/attrdict.py
Normal file
49
pnc/dep/attrdict.py
Normal 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
579
pnc/lexercon.py
Normal 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
601
pnc/unicolor.py
Normal 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
|
Loading…
Reference in a new issue