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"]
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.
@ -419,5 +419,5 @@ class BBJ(object):
print(usermap[author_id]["user_name"])
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"]

View File

@ -62,6 +62,11 @@ welcome = """>>> Welcome to Bulletin Butter & Jelly! ------------------@
@_________________________________________________________@
"""
colornames = [
"none", "red", "yellow", "green", "blue",
"cyan", "magenta"
]
colors = [
"\033[1;31m", "\033[1;33m", "\033[1;33m",
"\033[1;32m", "\033[1;34m", "\033[1;35m"
@ -90,10 +95,13 @@ class App(object):
("default", "default", "default"),
("bar", "light magenta", "default"),
("button", "light red", "default"),
("quote", "light green,underline", "default"),
("opt_prompt", "black", "light gray"),
("opt_header", "light cyan", "default"),
("hover", "light cyan", "default"),
("dim", "dark gray", "default"),
("bold", "default,bold", "default"),
("underline", "default,underline", "default"),
# map the bbj api color values for display
("0", "default", "default"),
@ -258,7 +266,7 @@ class App(object):
return [
head,
urwid.Divider(),
MessageBody(message["body"]),
urwid.Columns([(self.prefs["max_text_width"], MessageBody(message["body"]))]),
urwid.Divider(),
urwid.AttrMap(urwid.Divider("-"), "dim")
]
@ -333,7 +341,7 @@ class App(object):
if self.mode == "index":
self.last_pos = self.loop.widget.body.base_widget.get_focus()[1]
self.mode = "thread"
thread, usermap = network.thread_load(thread_id)
thread, usermap = network.thread_load(thread_id, format="sequential")
self.usermap.update(usermap)
self.thread = thread
self.walker.clear()
@ -480,6 +488,11 @@ class App(object):
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):
"""
@ -488,14 +501,13 @@ class App(object):
"""
editor_buttons = []
edit_mode = []
user_colors = []
if network.user_auth:
account_message = "Logged in as %s." % network.user_name
colors = ["None", "Red", "Yellow", "Green", "Blue", "Cyan", "Magenta"]
for index, color in enumerate(colors):
user_colors = []
for index, color in enumerate(colornames):
urwid.RadioButton(
user_colors, color,
user_colors, color.title(),
network.user["color"] == index,
self.set_color, index)
@ -513,11 +525,11 @@ class App(object):
account_stuff = [urwid.Button("Login/Register", on_press=self.relog)]
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"))
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"))
time_stuff = [
@ -528,7 +540,10 @@ class App(object):
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)
for editor in editors:
urwid.RadioButton(
@ -564,6 +579,9 @@ class App(object):
urwid.Divider(),
*time_stuff,
urwid.Divider(),
urwid.Text(("button", "Max message width:")),
urwid.AttrMap(width_edit, "opt_prompt"),
urwid.Divider(),
urwid.Text(("button", "Text editor:")),
urwid.Text("You can type in your own command or use one of these presets."),
urwid.Divider(),
@ -587,7 +605,7 @@ class App(object):
align="center",
valign="middle",
width=30,
height=(self.loop.screen_size[1] - 10)
height=("relative", 75)
)
@ -681,8 +699,8 @@ class App(object):
self.loop.widget,
align="center",
valign="middle",
width=self.loop.screen_size[0] - 2,
height=(self.loop.screen_size[1] - 4))
width=("relative", 90),
height=("relative", 80))
elif self.mode == "thread":
self.window_split=True
@ -701,10 +719,87 @@ class App(object):
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):
super(FootPrompt, self).__init__()
self.callback = callback
@ -728,6 +823,10 @@ class ExternalEditor(urwid.Terminal):
self.endpoint = endpoint
self.params = params
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"})
command = ["bash", "-c", "{} {}; echo Press any key to kill this window...".format(
app.prefs["editor"], self.path)]
@ -757,6 +856,16 @@ class ExternalEditor(urwid.Terminal):
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):
def keypress(self, size, key):
if key == "esc":

View File

@ -1,5 +1,5 @@
from src.exceptions import BBJException, BBJParameterError, BBJUserError
from src import db, schema
from src import db, schema, formatting
from functools import wraps
from uuid import uuid1
import traceback
@ -244,6 +244,9 @@ class API(object):
thread = db.thread_get(database, args["thread_id"])
cherrypy.thread_data.usermap = \
create_usermap(database, thread["messages"])
if args.get("format") == "sequential":
formatting.apply_formatting(thread["messages"],
formatting.sequential_expressions)
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.
"""
from markdown import markdown
from html import escape
test = """
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
#0, 1 2 3 4 5 6
colors = [
#0, 1 2 3 4 5 6
"red", "yellow", "green", "blue", "cyan", "magenta"
]
markup = [
"bold", "italic", "underline", "strike"
"bold", "italic", "underline", "linequote", "quote", "rainbow"
]
tokens = re.compile(r"\[(%s): (.+?)]" % "|".join(colors + markup),
flags=re.DOTALL)
# tokens being [red: this will be red] and [bold: this will be bold]
# 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]+)")
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):
"""
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)):
msg_obj[x]["body"] = formatter(msg_obj[x]["body"])
for x, obj in enumerate(msg_obj):
msg_obj[x]["body"] = formatter(obj["body"])
return msg_obj