primitive, awful text formatting
parent
28680865ee
commit
b731ab69fa
|
@ -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"]
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue