primitive, awful text formatting

pull/4/head
Blake DeMarcy 2017-04-11 15:31:01 -05:00
parent 28680865ee
commit b731ab69fa
4 changed files with 241 additions and 28 deletions

View File

@ -408,7 +408,7 @@ class BBJ(object):
return response["data"], response["usermap"] return response["data"], response["usermap"]
def thread_load(self, thread_id): def thread_load(self, thread_id, format=None):
""" """
Returns a tuple where [0] is a thread object and [1] is a usermap object. Returns a tuple where [0] is a thread object and [1] is a usermap object.
@ -419,5 +419,5 @@ class BBJ(object):
print(usermap[author_id]["user_name"]) print(usermap[author_id]["user_name"])
print(message["body"]) print(message["body"])
""" """
response = self("thread_load", thread_id=thread_id) response = self("thread_load", format=format, thread_id=thread_id)
return response["data"], response["usermap"] return response["data"], response["usermap"]

View File

@ -62,6 +62,11 @@ welcome = """>>> Welcome to Bulletin Butter & Jelly! ------------------@
@_________________________________________________________@ @_________________________________________________________@
""" """
colornames = [
"none", "red", "yellow", "green", "blue",
"cyan", "magenta"
]
colors = [ colors = [
"\033[1;31m", "\033[1;33m", "\033[1;33m", "\033[1;31m", "\033[1;33m", "\033[1;33m",
"\033[1;32m", "\033[1;34m", "\033[1;35m" "\033[1;32m", "\033[1;34m", "\033[1;35m"
@ -90,10 +95,13 @@ class App(object):
("default", "default", "default"), ("default", "default", "default"),
("bar", "light magenta", "default"), ("bar", "light magenta", "default"),
("button", "light red", "default"), ("button", "light red", "default"),
("quote", "light green,underline", "default"),
("opt_prompt", "black", "light gray"), ("opt_prompt", "black", "light gray"),
("opt_header", "light cyan", "default"), ("opt_header", "light cyan", "default"),
("hover", "light cyan", "default"), ("hover", "light cyan", "default"),
("dim", "dark gray", "default"), ("dim", "dark gray", "default"),
("bold", "default,bold", "default"),
("underline", "default,underline", "default"),
# map the bbj api color values for display # map the bbj api color values for display
("0", "default", "default"), ("0", "default", "default"),
@ -258,7 +266,7 @@ class App(object):
return [ return [
head, head,
urwid.Divider(), urwid.Divider(),
MessageBody(message["body"]), urwid.Columns([(self.prefs["max_text_width"], MessageBody(message["body"]))]),
urwid.Divider(), urwid.Divider(),
urwid.AttrMap(urwid.Divider("-"), "dim") urwid.AttrMap(urwid.Divider("-"), "dim")
] ]
@ -333,7 +341,7 @@ class App(object):
if self.mode == "index": if self.mode == "index":
self.last_pos = self.loop.widget.body.base_widget.get_focus()[1] self.last_pos = self.loop.widget.body.base_widget.get_focus()[1]
self.mode = "thread" self.mode = "thread"
thread, usermap = network.thread_load(thread_id) thread, usermap = network.thread_load(thread_id, format="sequential")
self.usermap.update(usermap) self.usermap.update(usermap)
self.thread = thread self.thread = thread
self.walker.clear() self.walker.clear()
@ -480,6 +488,11 @@ class App(object):
widget.set_text(rendered) widget.set_text(rendered)
def edit_width(self, editor, content):
self.prefs["max_text_width"] = \
int(content) if content else 0
bbjrc("update", **self.prefs)
def options_menu(self): def options_menu(self):
""" """
@ -488,14 +501,13 @@ class App(object):
""" """
editor_buttons = [] editor_buttons = []
edit_mode = [] edit_mode = []
user_colors = []
if network.user_auth: if network.user_auth:
account_message = "Logged in as %s." % network.user_name account_message = "Logged in as %s." % network.user_name
colors = ["None", "Red", "Yellow", "Green", "Blue", "Cyan", "Magenta"] user_colors = []
for index, color in enumerate(colors): for index, color in enumerate(colornames):
urwid.RadioButton( urwid.RadioButton(
user_colors, color, user_colors, color.title(),
network.user["color"] == index, network.user["color"] == index,
self.set_color, index) self.set_color, index)
@ -513,11 +525,11 @@ class App(object):
account_stuff = [urwid.Button("Login/Register", on_press=self.relog)] account_stuff = [urwid.Button("Login/Register", on_press=self.relog)]
time_box = urwid.Text(self.timestring(time(), "time")) time_box = urwid.Text(self.timestring(time(), "time"))
time_edit = urwid.Edit(edit_text=self.prefs["time"]) time_edit = Prompt(edit_text=self.prefs["time"])
urwid.connect_signal(time_edit, "change", self.live_time_render, (time_box, "time")) urwid.connect_signal(time_edit, "change", self.live_time_render, (time_box, "time"))
date_box = urwid.Text(self.timestring(time(), "date")) date_box = urwid.Text(self.timestring(time(), "date"))
date_edit = urwid.Edit(edit_text=self.prefs["date"]) date_edit = Prompt(edit_text=self.prefs["date"])
urwid.connect_signal(date_edit, "change", self.live_time_render, (date_box, "date")) urwid.connect_signal(date_edit, "change", self.live_time_render, (date_box, "date"))
time_stuff = [ time_stuff = [
@ -528,7 +540,10 @@ class App(object):
date_box, urwid.AttrMap(date_edit, "opt_prompt"), date_box, urwid.AttrMap(date_edit, "opt_prompt"),
] ]
editor_display = urwid.Edit(edit_text=self.prefs["editor"]) width_edit = urwid.IntEdit(default=self.prefs["max_text_width"])
urwid.connect_signal(width_edit, "change", self.edit_width)
editor_display = Prompt(edit_text=self.prefs["editor"])
urwid.connect_signal(editor_display, "change", self.set_new_editor, editor_buttons) urwid.connect_signal(editor_display, "change", self.set_new_editor, editor_buttons)
for editor in editors: for editor in editors:
urwid.RadioButton( urwid.RadioButton(
@ -564,6 +579,9 @@ class App(object):
urwid.Divider(), urwid.Divider(),
*time_stuff, *time_stuff,
urwid.Divider(), urwid.Divider(),
urwid.Text(("button", "Max message width:")),
urwid.AttrMap(width_edit, "opt_prompt"),
urwid.Divider(),
urwid.Text(("button", "Text editor:")), urwid.Text(("button", "Text editor:")),
urwid.Text("You can type in your own command or use one of these presets."), urwid.Text("You can type in your own command or use one of these presets."),
urwid.Divider(), urwid.Divider(),
@ -587,7 +605,7 @@ class App(object):
align="center", align="center",
valign="middle", valign="middle",
width=30, width=30,
height=(self.loop.screen_size[1] - 10) height=("relative", 75)
) )
@ -681,8 +699,8 @@ class App(object):
self.loop.widget, self.loop.widget,
align="center", align="center",
valign="middle", valign="middle",
width=self.loop.screen_size[0] - 2, width=("relative", 90),
height=(self.loop.screen_size[1] - 4)) height=("relative", 80))
elif self.mode == "thread": elif self.mode == "thread":
self.window_split=True self.window_split=True
@ -701,10 +719,87 @@ class App(object):
class MessageBody(urwid.Text): class MessageBody(urwid.Text):
pass def __init__(self, text_objects):
result = []
last_directive = None
for paragraph in text_objects:
for directive, body in paragraph:
if directive in colornames:
color = str(colornames.index(directive))
result.append((color, body))
elif directive in ["underline", "bold"]:
result.append((directive, body))
elif directive == "linequote":
if directive != last_directive and result[-1][-1][-1] != "\n":
result.append(("default", "\n"))
result.append(("3", "%s\n" % body.strip()))
elif directive == "quote":
result.append(("quote", ">>%s" % body))
elif directive == "rainbow":
color = 1
for char in body:
if color == 7:
color = 1
result.append((str(color), char))
color += 1
else:
result.append(("default", body))
last_directive = directive
result.append("\n\n")
result.pop()
super(MessageBody, self).__init__(result)
class FootPrompt(urwid.Edit): class Prompt(urwid.Edit):
"""
Supports basic bashmacs keybinds. Key casing is
ignored and ctrl/alt are treated the same. Only
character-wise (not word-wise) movements are
implemented.
"""
def keypress(self, size, key):
if not super(Prompt, self).keypress(size, key):
return
elif key[0:4] not in ["meta", "ctrl"]:
return key
column = self.get_cursor_coords((app.loop.screen_size[0],))[0]
text = self.get_edit_text()
key = key[-1].lower()
if key == "u":
self.set_edit_pos(0)
self.set_edit_text(text[column:])
elif key == "k":
self.set_edit_text(text[:column])
elif key == "f":
self.keypress(size, "right")
elif key == "b":
self.keypress(size, "left")
elif key == "a":
self.set_edit_pos(0)
elif key == "e":
self.set_edit_pos(len(text))
elif key == "d":
self.set_edit_text(text[0:column] + text[column+1:])
return key
class FootPrompt(Prompt):
def __init__(self, callback, *callback_args): def __init__(self, callback, *callback_args):
super(FootPrompt, self).__init__() super(FootPrompt, self).__init__()
self.callback = callback self.callback = callback
@ -728,6 +823,10 @@ class ExternalEditor(urwid.Terminal):
self.endpoint = endpoint self.endpoint = endpoint
self.params = params self.params = params
env = os.environ env = os.environ
# barring this, programs will happily spit out unicode chars which
# urwid+python3 seem to choke on. This seems to be a bug on urwid's
# behalf. Users who take issue to programs trying to supress unicode
# should use the options menu to switch to Overthrow mode.
env.update({"LANG": "POSIX"}) env.update({"LANG": "POSIX"})
command = ["bash", "-c", "{} {}; echo Press any key to kill this window...".format( command = ["bash", "-c", "{} {}; echo Press any key to kill this window...".format(
app.prefs["editor"], self.path)] app.prefs["editor"], self.path)]
@ -757,6 +856,16 @@ class ExternalEditor(urwid.Terminal):
app.switch_editor() app.switch_editor()
def __del__(self):
"""
Make damn sure we scoop up after ourselves here...
"""
try:
os.remove(self.path)
except FileNotFoundError:
pass
class OptionsMenu(urwid.LineBox): class OptionsMenu(urwid.LineBox):
def keypress(self, size, key): def keypress(self, size, key):
if key == "esc": if key == "esc":

View File

@ -1,5 +1,5 @@
from src.exceptions import BBJException, BBJParameterError, BBJUserError from src.exceptions import BBJException, BBJParameterError, BBJUserError
from src import db, schema from src import db, schema, formatting
from functools import wraps from functools import wraps
from uuid import uuid1 from uuid import uuid1
import traceback import traceback
@ -244,6 +244,9 @@ class API(object):
thread = db.thread_get(database, args["thread_id"]) thread = db.thread_get(database, args["thread_id"])
cherrypy.thread_data.usermap = \ cherrypy.thread_data.usermap = \
create_usermap(database, thread["messages"]) create_usermap(database, thread["messages"])
if args.get("format") == "sequential":
formatting.apply_formatting(thread["messages"],
formatting.sequential_expressions)
return thread return thread

View File

@ -3,34 +3,135 @@ This module is not complete and none of its functions are currently
used elsewhere. Subject to major refactoring. used elsewhere. Subject to major refactoring.
""" """
from markdown import markdown test = """
from html import escape This is a small paragraph
thats divided between a
few rows.
this opens a few linequotes.
>this is a few
>rows of
>sequential line breaks
and this is what follows right after
"""
# from markdown import markdown
# from html import escape
import re import re
#0, 1 2 3 4 5 6
colors = [ colors = [
#0, 1 2 3 4 5 6
"red", "yellow", "green", "blue", "cyan", "magenta" "red", "yellow", "green", "blue", "cyan", "magenta"
] ]
markup = [ markup = [
"bold", "italic", "underline", "strike" "bold", "italic", "underline", "linequote", "quote", "rainbow"
] ]
tokens = re.compile(r"\[(%s): (.+?)]" % "|".join(colors + markup), # tokens being [red: this will be red] and [bold: this will be bold]
flags=re.DOTALL) # tokens = re.compile(r"\[(%s): (.+?)]" % "|".join(colors + markup), flags=re.DOTALL)
# quotes being references to other post_ids, like >>34 or >>0 for OP
quotes = re.compile(">>([0-9]+)") quotes = re.compile(">>([0-9]+)")
linequotes = re.compile("^(>.+)$",
flags=re.MULTILINE) # linequotes being chan-style greentext,
# >like this
linequotes = re.compile("^(>.+)$", flags=re.MULTILINE)
def parse_segments(text, sanitize_linequotes=True):
"""
Parse linequotes, quotes, and paragraphs into their appropriate
representations. Paragraphs are represented as separate strings
in the returned list, and quote-types are compiled to their
[bracketed] representations.
"""
result = list()
for paragraph in [p.strip() for p in re.split("\n{2,}", text)]:
pg = str()
for segment in [s.strip() for s in paragraph.split("\n")]:
if not segment:
continue
segment = quotes.sub(lambda m: "[quote: %s]" % m.group(1), segment)
if segment.startswith(">"):
if sanitize_linequotes:
inner = segment.replace("]", "\\]")
else:
inner = segment
segment = "[linequote: %s]" % inner
# pg = pg[0:-1]
pg += segment
else:
pg += segment + " "
result.append(pg.strip())
return result
def sequential_expressions(string):
"""
Takes a string, sexpifies it, and returns a list of lists
who contain tuples. Each list of tuples represents a paragraph.
Within each paragraph, [0] is either None or a markup directive,
and [1] is the body of text to which it applies. This representation
is very easy to handle for a client. It semi-supports nesting:
eg, the expression [red: this [blue: is [green: mixed]]] will
return [("red", "this "), ("blue", "is "), ("green", "mixed")],
but this cannot effectively express an input like
[bold: [red: bolded colors.]], in which case the innermost
expression will take precedence. For the input:
"[bold: [red: this] is some shit [green: it cant handle]]"
you get:
[('red', 'this'), ('bold', ' is some shit '), ('green', 'it cant handle')]
"""
# abandon all hope ye who enter here
directives = colors + markup
result = list()
for paragraph in parse_segments(string):
stack = [[None, str()]]
skip_iters = []
nest = [None]
escaped = False
for index, char in enumerate(paragraph):
if skip_iters:
skip_iters.pop()
continue
if not escaped and char == "[":
directive = paragraph[index+1:paragraph.find(": ", index+1)]
open_p = directive in directives
else: open_p = False
clsd_p = not escaped and nest[-1] != None and char == "]"
# dont splice other directives into linequotes: that is far
# too confusing for the client to determine where to put line
# breaks
if open_p and nest[-1] != "linequote":
stack.append([directive, str()])
nest.append(directive)
[skip_iters.append(x) for x in range(len(directive)+2)]
elif clsd_p:
nest.pop()
stack.append([nest[-1], str()])
else:
escaped = char == "\\"
if not (escaped and paragraph[index+1] in "[]"):
stack[-1][1] += char
# filter out unused stacks, eg ["red", ""]
result.append([(directive, body) for directive, body in stack if body])
return result
def apply_formatting(msg_obj, formatter): def apply_formatting(msg_obj, formatter):
""" """
Receives a messages object from a thread and returns it with Receives a messages object from a thread and returns it with
all the message bodies passed through FORMATTER. all the message bodies passed through FORMATTER. Not all
formatting functions have to return a string. Refer to the
documentation for each formatter.
""" """
for x in range(len(msg_obj)): for x, obj in enumerate(msg_obj):
msg_obj[x]["body"] = formatter(msg_obj[x]["body"]) msg_obj[x]["body"] = formatter(obj["body"])
return msg_obj return msg_obj