haha i have a client now
parent
45a033670a
commit
31a2067514
415
bbj.el
415
bbj.el
|
@ -1,74 +1,305 @@
|
|||
(require 'json)
|
||||
(require 'cl)
|
||||
|
||||
;; stuff you can touch
|
||||
(defvar bbj:host "localhost")
|
||||
(defvar bbj:port "7066")
|
||||
(defvar bbj:width 80)
|
||||
|
||||
;; stuff you shouldnt touch
|
||||
(defvar bbj:logged-in nil)
|
||||
(defvar bbj:user nil)
|
||||
(defvar bbj:hash nil)
|
||||
|
||||
(make-variable-buffer-local
|
||||
(defvar bbj:*usermap* nil))
|
||||
(make-variable-buffer-local
|
||||
(defvar bbj:buffer-type nil))
|
||||
(make-variable-buffer-local
|
||||
(defvar bbj:aux-callback #'ignore))
|
||||
|
||||
(define-derived-mode bbj-mode fundamental-mode "[BBJ]"
|
||||
|
||||
(define-derived-mode bbj-mode fundamental-mode "BBJ"
|
||||
"Mode for browsing and posting to BBJ."
|
||||
:group 'bbj-mode
|
||||
(local-set-key (kbd "C-c C-c") 'bbj:aux)
|
||||
(local-set-key (kbd "+") 'bbj:compose)
|
||||
(local-set-key (kbd "RET") 'bbj:enter))
|
||||
(local-set-key (kbd "SPC") 'bbj:next-post)
|
||||
(local-set-key (kbd "j") 'bbj:next-post)
|
||||
(local-set-key (kbd "n") 'bbj:next-post)
|
||||
(local-set-key (kbd "<down>") 'bbj:next-post)
|
||||
|
||||
(defun bbj:request (&rest cells)
|
||||
(push (cons 'user bbj:user) cells)
|
||||
(push (cons 'auth_hash bbj:hash) cells)
|
||||
(with-temp-buffer
|
||||
(insert (json-encode cells))
|
||||
(shell-command-on-region
|
||||
(point-min) (point-max)
|
||||
(format "nc %s %s" bbj:host bbj:port)))
|
||||
(with-current-buffer "*Shell Command Output*"
|
||||
(goto-char (point-min))
|
||||
(let (json-false json-null)
|
||||
(json-read))))
|
||||
(local-set-key (kbd "DEL") 'bbj:prev-post)
|
||||
(local-set-key (kbd "k") 'bbj:prev-post)
|
||||
(local-set-key (kbd "p") 'bbj:prev-post)
|
||||
(local-set-key (kbd "<up>") 'bbj:prev-post)
|
||||
|
||||
(local-set-key (kbd "RET") 'bbj:enter)
|
||||
(local-set-key (kbd "l") 'bbj:enter)
|
||||
(local-set-key (kbd "o") 'bbj:enter)
|
||||
(local-set-key (kbd "<right>") 'bbj:enter)
|
||||
|
||||
(local-set-key (kbd "q") 'quit-window)
|
||||
(local-set-key (kbd "<left>") 'quit-window)
|
||||
|
||||
(local-set-key (kbd "+") 'bbj:compose)
|
||||
(local-set-key (kbd "c") 'bbj:compose)
|
||||
|
||||
(local-set-key (kbd "C-c C-c") 'bbj:aux)
|
||||
(local-set-key (kbd "r") 'bbj:quote-current-post))
|
||||
|
||||
(ignore-errors
|
||||
(evil-set-initial-state 'bbj-mode 'emacs))
|
||||
|
||||
;;;; network shit ;;;;
|
||||
|
||||
(defun bbj:descend (alist &rest keys)
|
||||
"Recursively retrieve symbols from a nested alist. A required beverage
|
||||
for all JSON tourists."
|
||||
(while keys
|
||||
(setq alist (alist-get (pop keys) alist)))
|
||||
alist)
|
||||
|
||||
|
||||
(defun bbj:request (method &rest pairs)
|
||||
"Poke netcat to poke the server who will hopefully poke us back"
|
||||
;; json-false/json-nil are bound as nil here to stop them from being silly keywords
|
||||
(let (json message json-false json-null
|
||||
(data (list
|
||||
(cons 'user bbj:user)
|
||||
(cons 'auth_hash bbj:hash)
|
||||
(cons 'method method))))
|
||||
;; populate a query with our hash and username, then the func arguments
|
||||
(while pairs
|
||||
(push (cons (pop pairs) (pop pairs)) data))
|
||||
|
||||
(with-temp-buffer
|
||||
(insert (json-encode data))
|
||||
(call-process-region
|
||||
(point-min) (point-max)
|
||||
shell-file-name t t nil ;; meow meow
|
||||
"-c" (format "nc %s %s" bbj:host bbj:port))
|
||||
(setq json (progn (goto-char (point-min)) (json-read))))
|
||||
|
||||
(if (eq json t) t ;; a few enpoints just return true/false
|
||||
(setq message (bbj:descend json 'error 'description))
|
||||
(case (bbj:descend json 'error 'code)
|
||||
;; haha epic handling
|
||||
((4 5 6 7) (error message))
|
||||
(otherwise json)))))
|
||||
|
||||
|
||||
(defun bbj:sethash (&optional password)
|
||||
"Either prompt for or take the arg `PASSWORD', and then sha256-hash it.
|
||||
Sets it globally and also returns it."
|
||||
(interactive)
|
||||
(unless password (setq password
|
||||
(read-from-minibuffer "(Password)> ")))
|
||||
(setq bbj:hash (secure-hash 'sha256 password)))
|
||||
|
||||
|
||||
(defun bbj:login ()
|
||||
"Prompts the user for a name and password. If it isn't registered, we'll take
|
||||
care of that. Jumps to the index afterward. This function only needs to be used
|
||||
once per emacs session."
|
||||
(interactive)
|
||||
(setq bbj:user (read-from-minibuffer "(BBJ Username)> "))
|
||||
(cond
|
||||
((bbj:request '(method . "is_registered")
|
||||
`(target_user . ,bbj:user))
|
||||
((bbj:request "is_registered" 'target_user bbj:user)
|
||||
(bbj:sethash)
|
||||
(unless (bbj:request '(method . "check_auth"))
|
||||
(message "(Invalid Password!)")))
|
||||
(if (bbj:request "check_auth")
|
||||
(progn
|
||||
(setq bbj:logged-in t)
|
||||
(bbj:browse-index)
|
||||
(message "Logged in as %s!" bbj:user))
|
||||
(message "(Invalid Password!)")
|
||||
(run-at-time 1 nil #'bbj:login)))
|
||||
((y-or-n-p (format "Register for BBJ as %s? " bbj:user))
|
||||
(let ((response (bbj:request
|
||||
(cons 'method "user_register")
|
||||
(cons 'auth_hash (bbj:sethash))
|
||||
(cons 'user bbj:user)
|
||||
(cons 'quip (read-from-minibuffer "(Quip)> "))
|
||||
(cons 'bio (read-from-minibuffer "(Bio)> ")))))
|
||||
(bbj:sethash)
|
||||
(let ((response
|
||||
(bbj:request "user_register"
|
||||
;; need to add some cute prompts for these
|
||||
'quip "" 'bio "")))
|
||||
(if (alist-get 'error response)
|
||||
(message "%s" (alist-get 'error response))
|
||||
(setq bbj:logged-in t)
|
||||
(bbj:browse-index)
|
||||
(message "Logged in as %s!" bbj:user))))))
|
||||
|
||||
|
||||
;;;; user navigation shit. a LOT of user navigation shit. ;;;;
|
||||
(defun bbj:next-pos (string &optional regex prop backward group bound)
|
||||
;; haha yes i ripped this from one of my other projects
|
||||
"Takes a STRING and returns the char position of the beginning of its
|
||||
next occurence from point in `current-buffer'. Returns nil if not found.
|
||||
A simpler way to call this is to use `bbj:next-prop'.
|
||||
|
||||
When REGEX is non-nil, STRING is interpreted as a regular expression.
|
||||
|
||||
PROP, when non-nil, will only return matches if they have the corresponding
|
||||
value for a property. This can either be a symbol or a cons cell. If it's
|
||||
a symbol, the property key used is 'type. As a cons, The key and expected
|
||||
value are given, eg '(type . end)
|
||||
|
||||
BACKWARD, when non-nil, does what it says on the tin.
|
||||
|
||||
When GROUP is non-nil and an integer, returns start pos of that match
|
||||
group. When PROP is in effect, it checks property at this position instead
|
||||
of 0.
|
||||
|
||||
BOUND can be a buffer position (integer) that the search will not exceed."
|
||||
(save-excursion
|
||||
(let ((search (if backward (if regex 're-search-backward 'search-backward)
|
||||
(if regex 're-search-forward 'search-forward)))
|
||||
(group (or group 0))
|
||||
(propkey (if (consp prop) (car prop) 'type))
|
||||
(propval (if (consp prop) (cdr prop) prop))
|
||||
found)
|
||||
(while (and (not found) (funcall search string bound t))
|
||||
(if prop (setq found
|
||||
(eql propval (get-char-property
|
||||
(match-beginning group) propkey)))
|
||||
(setq found t)))
|
||||
(when found
|
||||
(match-beginning group)))))
|
||||
|
||||
|
||||
(defun bbj:next-prop (prop &optional backward bound)
|
||||
"Like the `bbj:next-pos', but doesnt care about strings and
|
||||
just hunts for a specific text property."
|
||||
(bbj:next-pos "." t prop backward nil bound))
|
||||
|
||||
|
||||
(defun bbj:post-prop (prop &optional id)
|
||||
"retrieve PROP from the current post. needs ID-seeking support"
|
||||
(save-excursion
|
||||
(bbj:assert-post-start)
|
||||
(get-char-property (point) prop)))
|
||||
|
||||
|
||||
;; returns positions of the next head and ending seps, respectively
|
||||
(defun bbj:head-pos (&optional backward)
|
||||
(bbj:next-prop 'head backward))
|
||||
(defun bbj:sep-pos (&optional backward)
|
||||
(bbj:next-prop 'end backward))
|
||||
|
||||
|
||||
(defun bbj:assert-post-start ()
|
||||
(unless (eql 'head (get-char-property (point) 'type))
|
||||
(goto-char (bbj:head-pos t))))
|
||||
|
||||
|
||||
(defun bbj:point-to-post (dir &optional nocenter)
|
||||
"Move the cursor from the head of one post to another, in (symbol) DIR"
|
||||
(let ((check (case dir
|
||||
(prev (bbj:head-pos t))
|
||||
(next (save-excursion ;; or else point will stick
|
||||
(while (eq 'head (get-char-property (point) 'type))
|
||||
(goto-char (next-property-change (point))))
|
||||
(bbj:head-pos))))))
|
||||
(when check
|
||||
(goto-char check)
|
||||
(back-to-indentation)
|
||||
(unless nocenter (recenter 1)))))
|
||||
|
||||
|
||||
(defun bbj:next-post ()
|
||||
(interactive)
|
||||
(bbj:point-to-post 'next))
|
||||
|
||||
|
||||
(defun bbj:prev-post ()
|
||||
(interactive)
|
||||
(bbj:point-to-post 'prev))
|
||||
|
||||
|
||||
(defun bbj:first-post ()
|
||||
;; does interactive work like this? i never checked tbh
|
||||
(interactive (push-mark))
|
||||
(goto-char (+ 1 bbj:width (point-min))))
|
||||
|
||||
|
||||
(defun bbj:aux ()
|
||||
"just some random lazy callback shitty thing for C-c C-c"
|
||||
(interactive)
|
||||
(funcall bbj:aux-callback))
|
||||
|
||||
|
||||
(defun bbj:enter ()
|
||||
"Handles the RETURN key (and other similar binds) depending on
|
||||
content type. Currently only opens threads."
|
||||
(interactive)
|
||||
(case bbj:buffer-type
|
||||
(index
|
||||
(bbj:enter-thread
|
||||
(alist-get 'thread_id (bbj:post-prop 'data))))))
|
||||
|
||||
|
||||
(defun bbj:quote-current-post ()
|
||||
"Pop a composer, and insert the post number at point as a quote."
|
||||
(interactive)
|
||||
(case bbj:buffer-type
|
||||
(thread
|
||||
(let ((id (alist-get 'post_id (bbj:post-prop 'data))))
|
||||
(bbj:compose)
|
||||
(insert (format ">>%s\n\n" id))))
|
||||
(index
|
||||
;; recursion haha yes
|
||||
(let ((buffer (current-buffer)))
|
||||
(bbj:enter)
|
||||
(unless (equal buffer (current-buffer))
|
||||
(bbj:quote-current-post))))))
|
||||
|
||||
|
||||
(defun bbj:compose ()
|
||||
"Construct an appropriate callback to either create a thread or
|
||||
reply to one. Pops a new window; window is killed and the message
|
||||
is sent using C-c C-c."
|
||||
(interactive)
|
||||
(let ((params (case bbj:buffer-type
|
||||
(index
|
||||
`("Composing a new thread (C-c C-c to send)"
|
||||
(lambda ()
|
||||
(let* ((message (bbj:consume-window (current-buffer)))
|
||||
(request (bbj:request "thread_create"
|
||||
'body message
|
||||
'title ,(read-from-minibuffer "(Thread Title)> ")
|
||||
'tags ,(read-from-minibuffer "(Comma-seperated tags, if any)> "))))
|
||||
(if (numberp (bbj:descend request 'error 'code))
|
||||
(message "%s" request)
|
||||
(message "thread submitted")
|
||||
(bbj:browse-index))))))
|
||||
(thread
|
||||
`("Replying to thread (C-c C-c to send)"
|
||||
(lambda ()
|
||||
(let* ((message (bbj:consume-window (current-buffer)))
|
||||
(request (bbj:request "thread_reply"
|
||||
'body message 'thread_id ,thread-id)))
|
||||
(if (numberp (bbj:descend request 'error 'code))
|
||||
(message "%s" request)
|
||||
(message "reply submitted")
|
||||
(bbj:enter-thread ,thread-id)
|
||||
(goto-char (point-max))
|
||||
(bbj:point-to-post 'prev)
|
||||
(recenter nil)))))))))
|
||||
|
||||
(apply #'bbj:compose-in-window params)))
|
||||
|
||||
|
||||
(defun bbj:compose-in-window (title callback &rest cbargs)
|
||||
"Create a new buffer, pop it, set TITLE as the header line, and
|
||||
assign CALLBACK to C-c C-c."
|
||||
(let ((buffer (get-buffer-create "*BBJ: Compose*")))
|
||||
(pop-to-buffer buffer)
|
||||
(with-current-buffer buffer
|
||||
(erase-buffer)
|
||||
(bbj-mode)
|
||||
(text-mode)
|
||||
(use-local-map (copy-keymap text-mode-map))
|
||||
(local-set-key (kbd "C-c C-c") 'bbj:aux)
|
||||
(setq header-line-format title
|
||||
bbj:aux-callback callback))))
|
||||
|
||||
|
||||
(defun bbj:consume-window (buffer)
|
||||
"Consume all text in the current buffer, delete the window if
|
||||
it is one, and kill the buffer. Returns property-free string."
|
||||
(interactive)
|
||||
(with-current-buffer buffer
|
||||
(let ((content (buffer-substring-no-properties
|
||||
|
@ -77,8 +308,130 @@
|
|||
content)))
|
||||
|
||||
|
||||
(defun bbj:postprocess ()
|
||||
"Makes all the whitespace in and between posts consistent."
|
||||
(bbj:first-post)
|
||||
(save-excursion
|
||||
(while (re-search-forward "\n\n\n+" nil t)
|
||||
(replace-match "\n\n"))))
|
||||
|
||||
|
||||
(defun bbj:render-body (string &optional return-string notrim)
|
||||
"takes an html STRING. If RETURN-STRING is non nil, it renders
|
||||
it in a temp buffer and returns the string. Otherwise, inserts
|
||||
and renders the content in the current buffer."
|
||||
(let* ((shr-width bbj:width)
|
||||
(shr-external-rendering-functions
|
||||
'((span . bbj:render-tag-span)))
|
||||
result)
|
||||
(if (not return-string)
|
||||
(let ((start (point)))
|
||||
(insert string)
|
||||
(shr-render-region start (point-max))
|
||||
(insert "\n\n"))
|
||||
(setq result
|
||||
(with-temp-buffer
|
||||
(insert string)
|
||||
(shr-render-region (point-min) (point-max))
|
||||
(buffer-substring (point-min) (point-max))))
|
||||
(if notrim result (string-trim result)))))
|
||||
|
||||
|
||||
(defun bbj:timestring (epoch)
|
||||
"Make a cute timestring out of the epoch (for post heads)"
|
||||
(format-time-string "%H:%M %a %m/%d/%y" epoch))
|
||||
|
||||
|
||||
(defun bbj:render-post (object)
|
||||
"Render an API object into the current buffer. Can be either the parent object
|
||||
or any of its children."
|
||||
(let* ((userdata (cdr (assoc-string (alist-get 'author object) bbj:*usermap*)))
|
||||
(title (alist-get 'title object))
|
||||
(indicator (format ">>%s " (or title (alist-get 'post_id object)))))
|
||||
(insert (propertize indicator
|
||||
'face 'font-lock-function-name-face
|
||||
'type 'head 'data object))
|
||||
(when title (insert "\n"))
|
||||
(insert (propertize
|
||||
(concat "~" (alist-get 'name userdata) " ")
|
||||
'face 'font-lock-keyword-face))
|
||||
(insert (if (eq bbj:buffer-type 'index)
|
||||
(propertize (format "@ %s\n%s replies; last active %s\n"
|
||||
(bbj:timestring (alist-get 'created object))
|
||||
(alist-get 'reply_count object)
|
||||
(bbj:timestring (alist-get 'lastmod object)))
|
||||
'face 'font-lock-comment-face)
|
||||
(propertize (format "@ %s\n\n" (bbj:timestring (alist-get 'created object)))
|
||||
'face 'font-lock-comment-face)))
|
||||
(when (eq bbj:buffer-type 'thread)
|
||||
(bbj:render-body (alist-get 'body object)))
|
||||
(bbj:insert-sep)))
|
||||
|
||||
|
||||
(defun bbj:render-tag-span (dom)
|
||||
"A slightly modified version of `shr-tag-span' which handles quotes and stuff.."
|
||||
(let ((class (dom-attr dom 'class)))
|
||||
(dolist (sub (dom-children dom))
|
||||
(if (stringp sub)
|
||||
(cond
|
||||
((equal class "quote")
|
||||
(insert (propertize sub
|
||||
'face 'font-lock-constant-face
|
||||
'type 'quote)))
|
||||
((equal class "linequote")
|
||||
(insert (propertize sub
|
||||
'face 'font-lock-string-face
|
||||
'type 'linequote)))
|
||||
(t (shr-insert sub)))
|
||||
(shr-descend sub)))))
|
||||
|
||||
|
||||
(defun bbj:mksep ()
|
||||
(format "\n%s\n" (make-string bbj:width ?\-)))
|
||||
|
||||
|
||||
(defun bbj:insert-sep (&optional drop-newline)
|
||||
(let ((sep (bbj:mksep)))
|
||||
(insert (propertize
|
||||
(if drop-newline (subseq sep 1) sep)
|
||||
'face 'font-lock-comment-face
|
||||
'type 'end))))
|
||||
|
||||
|
||||
(defun bbj:browse-index ()
|
||||
(interactive)
|
||||
(let ((response (bbj:request '(method . "thread_index"))))
|
||||
(cl-loop for thread across (alist-get 'threads response) do
|
||||
(message "%s" thread))))
|
||||
(let* ((inhibit-read-only t)
|
||||
(buffer (get-buffer-create "*BBJ Index*"))
|
||||
(response (bbj:request "thread_index"))
|
||||
(bbj:*usermap* (alist-get 'usermap response)))
|
||||
(with-current-buffer buffer
|
||||
(erase-buffer)
|
||||
(bbj-mode)
|
||||
(setq bbj:buffer-type 'index
|
||||
bbj:*usermap* (alist-get 'usermap response))
|
||||
(bbj:insert-sep t)
|
||||
(loop for thread across (alist-get 'threads response) do
|
||||
(bbj:render-post thread))
|
||||
(bbj:postprocess))
|
||||
(switch-to-buffer buffer)
|
||||
(setq buffer-read-only t)))
|
||||
|
||||
|
||||
(defun bbj:enter-thread (id)
|
||||
(interactive)
|
||||
(let* ((inhibit-read-only t)
|
||||
(response (bbj:request "thread_load" 'thread_id id))
|
||||
(buffer (get-buffer-create (format "BBJ: %s" (alist-get 'title response)))))
|
||||
(with-current-buffer buffer
|
||||
(erase-buffer)
|
||||
(bbj-mode)
|
||||
(setq bbj:buffer-type 'thread
|
||||
bbj:*usermap* (alist-get 'usermap response))
|
||||
(setq-local thread-id id)
|
||||
(bbj:insert-sep t)
|
||||
(bbj:render-post response)
|
||||
(loop for reply across (alist-get 'replies response) do
|
||||
(bbj:render-post reply))
|
||||
(bbj:postprocess))
|
||||
(switch-to-buffer buffer)
|
||||
(setq buffer-read-only t)))
|
||||
|
|
24
src/db.py
24
src/db.py
|
@ -1,3 +1,4 @@
|
|||
from src import formatting
|
||||
from uuid import uuid1
|
||||
from src import schema
|
||||
from time import time
|
||||
|
@ -24,10 +25,10 @@ except FileNotFoundError:
|
|||
|
||||
### THREAD MANAGEMENT ###
|
||||
|
||||
def thread_index(key="lastmod"):
|
||||
def thread_index(key="lastmod", markup=True):
|
||||
result = list()
|
||||
for ID in path.os.listdir(path.join(PATH, "threads")):
|
||||
thread = thread_load(ID)
|
||||
thread = thread_load(ID, markup)
|
||||
thread.pop("replies")
|
||||
result.append(thread)
|
||||
return sorted(result, key=lambda i: i[key], reverse=True)
|
||||
|
@ -42,10 +43,17 @@ def thread_create(author, body, title, tags):
|
|||
return scheme
|
||||
|
||||
|
||||
def thread_load(ID):
|
||||
def thread_load(ID, markup=True):
|
||||
try:
|
||||
with open(path.join(PATH, "threads", ID), "r") as f:
|
||||
return json.loads(f.read())
|
||||
thread = json.loads(f.read())
|
||||
if not markup:
|
||||
thread["body"] = formatting.cleanse(thread["body"])
|
||||
for x in range(len(thread["replies"])):
|
||||
thread["replies"][x]["body"] = formatting.cleanse(
|
||||
thread["replies"][x]["body"])
|
||||
return thread
|
||||
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
@ -62,7 +70,13 @@ def thread_reply(ID, author, body):
|
|||
|
||||
thread["reply_count"] += 1
|
||||
thread["lastmod"] = time()
|
||||
reply = schema.reply(thread["reply_count"], author, body)
|
||||
|
||||
if thread["replies"]:
|
||||
lastpost = thread["replies"][-1]["post_id"]
|
||||
else:
|
||||
lastpost = 1
|
||||
|
||||
reply = schema.reply(lastpost + 1, author, body)
|
||||
thread["replies"].append(reply)
|
||||
thread_dump(ID, thread)
|
||||
return reply
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from src import formatting
|
||||
from src import schema
|
||||
from src import db
|
||||
from json import dumps
|
||||
|
@ -48,13 +49,12 @@ def user_register(json):
|
|||
|
||||
|
||||
def thread_index(json):
|
||||
index = db.thread_index()
|
||||
return schema.response(
|
||||
{"threads": index}, create_usermap(index, True))
|
||||
index = db.thread_index(markup=not json.get("nomarkup"))
|
||||
return schema.response({"threads": index}, create_usermap(index, True))
|
||||
|
||||
|
||||
def thread_load(json):
|
||||
thread = db.thread_load(json["thread_id"])
|
||||
thread = db.thread_load(json["thread_id"], not json.get("nomarkup"))
|
||||
if not thread:
|
||||
return schema.error(7, "Requested thread does not exist")
|
||||
return schema.response(thread, create_usermap(thread))
|
||||
|
@ -66,12 +66,16 @@ def thread_create(json):
|
|||
json["body"],
|
||||
json["title"],
|
||||
json["tags"])
|
||||
return schema.response(thread, create_usermap(thread))
|
||||
if json.get("nomarkup"):
|
||||
thread["body"] = formatting.cleanse(thread["body"])
|
||||
return schema.response(thread)
|
||||
|
||||
|
||||
def thread_reply(json):
|
||||
thread = db.thread_reply(
|
||||
reply = db.thread_reply(
|
||||
json["thread_id"],
|
||||
json["user"],
|
||||
json["body"])
|
||||
return schema.response(thread)
|
||||
if json.get("nomarkup"):
|
||||
reply["body"] = formatting.cleanse(reply["body"])
|
||||
return schema.response(reply)
|
||||
|
|
|
@ -1,35 +1,28 @@
|
|||
from markdown import markdown
|
||||
from html import escape
|
||||
import re
|
||||
|
||||
# these parameters are utter nonsense...
|
||||
COLORS = ["red", "green", "yellow", "blue", "magenta", "cyan"]
|
||||
KEYWORDS = COLORS + [
|
||||
"bold", "italic", "underline"
|
||||
]
|
||||
MARKUP = ["bold", "italic", "underline", "strike"]
|
||||
TOKENS = re.compile(r"\[({}): (.+?)]".format("|".join(COLORS + MARKUP)), flags=re.DOTALL)
|
||||
QUOTES = re.compile(">>([0-9]+)")
|
||||
LINEQUOTES = re.compile("^(>.+)$", flags=re.MULTILINE)
|
||||
|
||||
|
||||
TOKENS = re.compile(r"\[\[({}): (.+?)]]".format("|".join(KEYWORDS)), flags=re.DOTALL)
|
||||
QUOTES = re.compile(">>([0-9]+)")
|
||||
LINEQUOTES = re.compile("^>(.+)$", flags=re.MULTILINE)
|
||||
def map_html(match):
|
||||
directive, body = match.group(1).lower(), match.group(2)
|
||||
if directive in COLORS:
|
||||
return '<span color="{0}" style="color: {0};">{1}</span>'.format(directive, body)
|
||||
elif directive in MARKUP:
|
||||
return '<{0}>{1}</{0}>'.format(directive[0], body)
|
||||
return body
|
||||
|
||||
|
||||
def parse(text, doquotes=True):
|
||||
output = TOKENS.sub("\\2", text)
|
||||
objects = list()
|
||||
offset = 0
|
||||
for token in TOKENS.finditer(text):
|
||||
directive = token.group(1).lower()
|
||||
start = token.start() - offset
|
||||
end = start + len(token.group(2))
|
||||
offset += len(directive) + 6
|
||||
if directive in COLORS:
|
||||
objects.append(["color", start, end, directive])
|
||||
else:
|
||||
objects.append([directive, start, end])
|
||||
|
||||
objects += [["linequote", m.start(), m.end()]
|
||||
for m in LINEQUOTES.finditer(output)]
|
||||
|
||||
text = TOKENS.sub(map_html, escape(text))
|
||||
if doquotes:
|
||||
objects += [["quote", m.start(), m.end(), int(m.group(1))]
|
||||
for m in QUOTES.finditer(output)]
|
||||
|
||||
return output, objects
|
||||
text = QUOTES.sub(r'<span post="\1" class="quote">\g<0></span>', text)
|
||||
return markdown(
|
||||
LINEQUOTES.sub(r'<span class="linequote">\1</span>', text)
|
||||
)
|
||||
|
|
|
@ -3,7 +3,6 @@ from time import time
|
|||
|
||||
def base():
|
||||
return {
|
||||
"usermap": {},
|
||||
"error": False
|
||||
}
|
||||
|
||||
|
@ -49,13 +48,13 @@ def user_external(ID, name, quip, bio, admin):
|
|||
|
||||
|
||||
def thread(ID, author, body, title, tags):
|
||||
text, entities = formatting.parse(body, doquotes=False)
|
||||
body = formatting.parse(body, doquotes=False)
|
||||
now = time()
|
||||
return {
|
||||
"thread_id": ID,
|
||||
"post_id": 1,
|
||||
"author": author,
|
||||
"body": text,
|
||||
"entities": entities, # of type list()
|
||||
"body": body,
|
||||
"title": title,
|
||||
"tags": tags,
|
||||
"replies": list(),
|
||||
|
@ -67,13 +66,12 @@ def thread(ID, author, body, title, tags):
|
|||
|
||||
|
||||
def reply(ID, author, body):
|
||||
text, entities = formatting.parse(body)
|
||||
body = formatting.parse(body)
|
||||
now = time()
|
||||
return {
|
||||
"post_id": ID,
|
||||
"author": author,
|
||||
"body": text,
|
||||
"entities": entities, # of type list()
|
||||
"body": body,
|
||||
"lastmod": now,
|
||||
"edited": False,
|
||||
"created": now
|
||||
|
|
Loading…
Reference in New Issue