Fun new stuff in site-lisp
This commit is contained in:
parent
d68cc8afb9
commit
f488db1819
6 changed files with 5360 additions and 0 deletions
139
site-lisp/50-arc.el
Normal file
139
site-lisp/50-arc.el
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
;; arc --- Summary
|
||||||
|
;;; Commentary:
|
||||||
|
;;; A bunch of helpers for Phabricator and arc
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'dash)
|
||||||
|
(require 'dash-functional)
|
||||||
|
(require 'ido)
|
||||||
|
(require 'image)
|
||||||
|
(require 'json)
|
||||||
|
(require 'projectile)
|
||||||
|
(require 's)
|
||||||
|
|
||||||
|
(defconst arc-binary "arc")
|
||||||
|
(defconst phabricator-macro-dir "~/public_html")
|
||||||
|
(defconst phabricator-macro-list "~tulloch/.arc_macros")
|
||||||
|
(defconst phabricator-macro-refresh-script "~tulloch/bin/arc_macros.sh")
|
||||||
|
|
||||||
|
(defcustom arc-repo-prefix-list
|
||||||
|
'(("fbcode" . "https://phabricator.fb.com/diffusion/FBCODE/browse/master")
|
||||||
|
("www" . "https://phabricator.fb.com/diffusion/E/browse/tfb/trunk/www"))
|
||||||
|
"*Mapping from repository name to path in diffusion."
|
||||||
|
:group 'arc)
|
||||||
|
|
||||||
|
(defun arc--call-conduit (method req)
|
||||||
|
"Call conduit METHOD with the parameters REQ."
|
||||||
|
(with-temp-buffer
|
||||||
|
(let ((tmp-file (make-temp-file "arc-conduit")))
|
||||||
|
(with-temp-file tmp-file (->> req json-encode insert))
|
||||||
|
(call-process
|
||||||
|
arc-binary tmp-file `(,(current-buffer) nil) nil "call-conduit" method)
|
||||||
|
(->> (buffer-string) json-read-from-string (assoc 'response) cdr))))
|
||||||
|
|
||||||
|
;;; ----------------------------------------------------------------------------
|
||||||
|
;;; arc macro commands.
|
||||||
|
(defun arc--list-macros ()
|
||||||
|
"Load phabricator-macro-list into an ido completion window."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents phabricator-macro-list)
|
||||||
|
(->> (buffer-string) (s-split "\n") (ido-completing-read "Macro: "))))
|
||||||
|
|
||||||
|
(defun arc-refresh-macros ()
|
||||||
|
"Refresh phabricator-macro-list from Phabricator with the latest macros."
|
||||||
|
(interactive)
|
||||||
|
(call-process-shell-command
|
||||||
|
phabricator-macro-refresh-script nil nil nil phabricator-macro-list))
|
||||||
|
|
||||||
|
(defun arc-insert-macro (name)
|
||||||
|
"Insert image NAME from a list of Phabricator macros."
|
||||||
|
(interactive `(,(arc--list-macros)))
|
||||||
|
(if (display-graphic-p)
|
||||||
|
(let ((img (->> name arc--get-macro create-image)))
|
||||||
|
(when (image-animated-p img)
|
||||||
|
(image-animate img 0 t))
|
||||||
|
(insert-image img name)
|
||||||
|
(insert "\n\n"))
|
||||||
|
(insert name)))
|
||||||
|
|
||||||
|
(defun arc--get-macro (macro-name)
|
||||||
|
"Retrieve the given MACRO-NAME image and save it to *phabricator-macro-dir*."
|
||||||
|
(interactive `(,(arc--list-macros)))
|
||||||
|
(let* ((macro-file-name (format "%s/%s" phabricator-macro-dir macro-name))
|
||||||
|
(download-uri
|
||||||
|
(lambda (uri)
|
||||||
|
(when (not (file-exists-p macro-file-name))
|
||||||
|
(url-copy-file uri macro-file-name)
|
||||||
|
(shell-command (format "chmod 644 %s" macro-file-name)))
|
||||||
|
macro-file-name))
|
||||||
|
(extract-uri
|
||||||
|
(lambda (output)
|
||||||
|
(->> output (assoc (intern macro-name)) cdr (assoc 'uri) cdr))))
|
||||||
|
(->> (arc--call-conduit "macro.query" `(:names (,macro-name)))
|
||||||
|
(funcall extract-uri) (funcall download-uri) message)))
|
||||||
|
|
||||||
|
|
||||||
|
;;; ----------------------------------------------------------------------------
|
||||||
|
;;; arc paste commands.
|
||||||
|
(defun arc-paste (start end)
|
||||||
|
"Pastes the specified region from START to END (or whole file) with arcanist.
|
||||||
|
The resulting URL is stored in the kill
|
||||||
|
ring and messaged in the minibuffer."
|
||||||
|
(interactive (if (use-region-p) `(,(region-beginning) ,(region-end))
|
||||||
|
`(,(point-min) ,(point-max))))
|
||||||
|
(let* ((extract-uri (lambda (output) (->> output (assoc 'uri) cdr))))
|
||||||
|
(->> (arc--call-conduit "paste.create"
|
||||||
|
`(:title ,(when (buffer-file-name) (file-name-nondirectory (buffer-file-name)))
|
||||||
|
:content ,(buffer-substring start end)))
|
||||||
|
(funcall extract-uri) message kill-new)))
|
||||||
|
|
||||||
|
|
||||||
|
;;; ----------------------------------------------------------------------------
|
||||||
|
;;; arc inlines commands
|
||||||
|
(defun arc-inlines ()
|
||||||
|
"Display the inlines for the current branch in a compilation buffer."
|
||||||
|
(interactive)
|
||||||
|
(let ((previous-dir default-directory))
|
||||||
|
(unwind-protect
|
||||||
|
(save-excursion
|
||||||
|
(cd (projectile-project-root))
|
||||||
|
(compile (format "%s inlines" arc-binary)))
|
||||||
|
(cd previous-dir))))
|
||||||
|
|
||||||
|
;;; ----------------------------------------------------------------------------
|
||||||
|
;;; arc browse commands
|
||||||
|
(defun arc--repo-prefix ()
|
||||||
|
"So fbcode2, fbcode_contbuild dirs should still map to fbcode."
|
||||||
|
(->> (-first
|
||||||
|
(lambda (p) (s-starts-with? (car p) (projectile-project-name)))
|
||||||
|
arc-repo-prefix-list)
|
||||||
|
cdr))
|
||||||
|
|
||||||
|
(defun arc-browse (start end)
|
||||||
|
"Paste the specified region from START to END (or current line).
|
||||||
|
The resulting URL is stored in the kill ring and messaged in the
|
||||||
|
minibuffer."
|
||||||
|
(interactive (if (use-region-p)
|
||||||
|
(mapc 'line-number-at-pos
|
||||||
|
`(,(region-beginning) ,(region-end)))
|
||||||
|
`(,(line-number-at-pos) ,(line-number-at-pos))))
|
||||||
|
(when (not (arc--repo-prefix))
|
||||||
|
(error "Not in a known Diffusion repository"))
|
||||||
|
(let* ((url (format "%s/%s$%s-%s"
|
||||||
|
(arc--repo-prefix)
|
||||||
|
(file-relative-name
|
||||||
|
(file-truename buffer-file-name)
|
||||||
|
(file-truename (projectile-project-root)))
|
||||||
|
start end)))
|
||||||
|
(->> url message kill-new)))
|
||||||
|
|
||||||
|
|
||||||
|
;;; ----------------------------------------------------------------------------
|
||||||
|
;;; Shortcuts
|
||||||
|
(global-set-key (kbd "C-c C-a i") 'arc-inlines)
|
||||||
|
(global-set-key (kbd "C-c C-a b") 'arc-browse)
|
||||||
|
(global-set-key (kbd "C-c C-a p") 'arc-paste)
|
||||||
|
(global-set-key (kbd "C-c C-a m") 'arc-insert-macro)
|
||||||
|
|
||||||
|
(provide '50-arc)
|
||||||
|
;;; 50-arc.el ends here
|
||||||
4720
site-lisp/color.term.el
Normal file
4720
site-lisp/color.term.el
Normal file
File diff suppressed because it is too large
Load diff
207
site-lisp/fb-note-publish.el
Normal file
207
site-lisp/fb-note-publish.el
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
;;; fb-note-publish.el --- Publish a markdown doc as a note -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2017 John Doty
|
||||||
|
|
||||||
|
;; Author: John Doty <doty@fb.com>
|
||||||
|
;; Keywords: wp, comm
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; This module provides some functions for publishing a markdown document as
|
||||||
|
;; a facebook note. (It's derived from the disseminate project's publishing
|
||||||
|
;; library, and it uses the same interngraph endpoint.)
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
(require 'json)
|
||||||
|
(require 'subr-x)
|
||||||
|
(require 'url)
|
||||||
|
(require 'url-vars)
|
||||||
|
|
||||||
|
;; This is the unique app-id for authenticating to interngraph.
|
||||||
|
(defconst fbn--app-id "1910641509202249")
|
||||||
|
|
||||||
|
;; Go ahead and change this to '$USER.sb' if you want to test against your
|
||||||
|
;; devserver.
|
||||||
|
(defconst fbn--interngraph-tier "intern")
|
||||||
|
|
||||||
|
(defun fbn--keychain (secret secret-group &optional secrets-tool-path)
|
||||||
|
"Extract the SECRET in SECRET-GROUP using the secret_tool at SECRETS-TOOL-PATH."
|
||||||
|
(let ((tool-path (or secrets-tool-path "/usr/local/bin/secrets_tool")))
|
||||||
|
(with-temp-buffer
|
||||||
|
(call-process tool-path nil t nil "get_from_group" secret secret-group)
|
||||||
|
(string-trim (buffer-string)))))
|
||||||
|
|
||||||
|
(defun fbn--get-secret-token ()
|
||||||
|
"Extract the secret interngraph token for the disseminate library."
|
||||||
|
(fbn--keychain "INTERNGRAPH_TOKEN" "FBNOTE_PUBLISH"))
|
||||||
|
|
||||||
|
(defun fbn--intern-graph-url (path)
|
||||||
|
"Construct the url to PATH on the intern graph."
|
||||||
|
(concat "https://interngraph." fbn--interngraph-tier ".facebook.com/" path))
|
||||||
|
|
||||||
|
(defun fbn--get-unixname ()
|
||||||
|
"Get the unixname of the currently executing user."
|
||||||
|
(getenv "USER"))
|
||||||
|
|
||||||
|
(defun fbn--invoke-json (path params)
|
||||||
|
"Invoke the intern graph PATH with PARAMS.
|
||||||
|
|
||||||
|
Params should be a list of key/value cons cells describing form request data.
|
||||||
|
We return the resulting JSON, decoded."
|
||||||
|
(let
|
||||||
|
((url (fbn--intern-graph-url path))
|
||||||
|
(url-request-method "POST")
|
||||||
|
(url-proxy-services nil)
|
||||||
|
(url-request-data
|
||||||
|
(mapconcat (lambda (pair)
|
||||||
|
(format "%s=%s"
|
||||||
|
(car pair)
|
||||||
|
(url-hexify-string (cdr pair))))
|
||||||
|
params
|
||||||
|
"&")))
|
||||||
|
(with-current-buffer (url-retrieve-synchronously url)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(re-search-forward "^$")
|
||||||
|
(json-read)
|
||||||
|
)))
|
||||||
|
|
||||||
|
(defun fbn--publish (title note-id note-html)
|
||||||
|
"Publish a note with specified TITLE, NOTE-ID, and NOTE-HTML.
|
||||||
|
|
||||||
|
NOTE-ID may be nil to create a new note, or the ID of an existing note. This
|
||||||
|
function returns the ID of the created note."
|
||||||
|
(let ((json-response (fbn--invoke-json
|
||||||
|
"research/note/"
|
||||||
|
`((app . ,fbn--app-id)
|
||||||
|
(token . ,(fbn--get-secret-token))
|
||||||
|
(title . ,title)
|
||||||
|
(unixname . ,(fbn--get-unixname))
|
||||||
|
(note_id . ,note-id)
|
||||||
|
(html . ,note-html)))))
|
||||||
|
(cdr (assoc 'note_id json-response))))
|
||||||
|
|
||||||
|
(defun fbn--convert (&optional buffer)
|
||||||
|
"Convert the specified BUFFER to the correct html.
|
||||||
|
|
||||||
|
Here we force the use of pandoc since we don't want to have to do
|
||||||
|
that silly thing where we split out the YAML frontmatter first,
|
||||||
|
and of course we expect that frontmatter to exist so that we can
|
||||||
|
stash the note ID there."
|
||||||
|
(let ((in-buffer (or buffer (current-buffer))))
|
||||||
|
(with-temp-buffer
|
||||||
|
(let ((out-buffer (current-buffer)))
|
||||||
|
(with-current-buffer in-buffer
|
||||||
|
(call-process-region (point-min) (point-max)
|
||||||
|
"pandoc" nil out-buffer nil
|
||||||
|
"-f" "markdown+yaml_metadata_block"
|
||||||
|
"-t" "html"
|
||||||
|
))
|
||||||
|
(string-trim (buffer-string))))))
|
||||||
|
|
||||||
|
(defun fbn--frontmatter-end (&optional buffer)
|
||||||
|
"Go to the end of the frontmatter in BUFFER."
|
||||||
|
(let ((in-buffer (or buffer (current-buffer))))
|
||||||
|
(with-current-buffer in-buffer
|
||||||
|
(let ((frontmatter-start))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(unless (looking-at "^---$")
|
||||||
|
(error "This buffer does not have frontmatter"))
|
||||||
|
(setq frontmatter-start (match-end 0))
|
||||||
|
(goto-char frontmatter-start)
|
||||||
|
(unless (re-search-forward "^---$" (point-max) t)
|
||||||
|
(error "Cannot find the end of the frontmatter"))
|
||||||
|
(goto-char (match-beginning 0))
|
||||||
|
(point)))))
|
||||||
|
|
||||||
|
(defun fbn--unquote (value)
|
||||||
|
"Unquote the specified VALUE, if it is quoted."
|
||||||
|
(cond
|
||||||
|
;; Appears to be quoted.
|
||||||
|
((string-prefix-p "\"" value)
|
||||||
|
(let* ((value (replace-regexp-in-string "\\\\\\\"" "\"" value))
|
||||||
|
(value (substring value 1 -1)))
|
||||||
|
value))
|
||||||
|
;; Not quoted.
|
||||||
|
(t value)))
|
||||||
|
|
||||||
|
(ert-deftest fbn-test-unquote ()
|
||||||
|
"Test that unquoting front matter values works properly"
|
||||||
|
(should (equal (fbn--unquote "Foo") "Foo"))
|
||||||
|
(should (equal (fbn--unquote "\"Foo\"") "Foo"))
|
||||||
|
(should (equal (fbn--unquote "\" \\\" \"") " \" ")))
|
||||||
|
|
||||||
|
(defun fbn--frontmatter-field (field buffer)
|
||||||
|
"Fetch the specified FIELD from the specified BUFFER.
|
||||||
|
|
||||||
|
If successful, this function returns the field value (as a
|
||||||
|
string), and also leaves the point at the beginning of the note
|
||||||
|
entry."
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(goto-char (fbn--frontmatter-end buffer))
|
||||||
|
(if (re-search-backward (concat "^" field ": \\(\.\+\\)") nil t)
|
||||||
|
(fbn--unquote (match-string-no-properties 1)))))
|
||||||
|
|
||||||
|
(defun fbn--set-frontmatter-field (field value buffer)
|
||||||
|
"Set the specified FIELD to VALUE in the specified BUFFER.
|
||||||
|
|
||||||
|
This replaces the existing field value, if present, otherwise it
|
||||||
|
adds it."
|
||||||
|
(let ((in-buffer (or buffer (current-buffer))))
|
||||||
|
(with-current-buffer in-buffer
|
||||||
|
(let ((existing-value))
|
||||||
|
(setq existing-value (fbn--frontmatter-field field buffer))
|
||||||
|
(unless (equal value existing-value)
|
||||||
|
(if existing-value
|
||||||
|
(delete-region (line-beginning-position)
|
||||||
|
(progn (forward-line 1) (point))))
|
||||||
|
(fbn--frontmatter-end buffer)
|
||||||
|
(insert (concat field ": " value "\n"))
|
||||||
|
)))))
|
||||||
|
|
||||||
|
(defun fbn--note-id (&optional buffer)
|
||||||
|
"Get the existing note ID out of BUFFER.
|
||||||
|
|
||||||
|
If successful, this function returns the note ID (as a string), and also leaves
|
||||||
|
the point at the beginning of the note entry."
|
||||||
|
(fbn--frontmatter-field "fbnote" buffer))
|
||||||
|
|
||||||
|
(defun fbn--note-title (&optional buffer)
|
||||||
|
"Get the existing note title out of BUFFER.
|
||||||
|
|
||||||
|
If successful, this function returns the note title (as a
|
||||||
|
string), and also leaves the point at the beginning of the note
|
||||||
|
entry."
|
||||||
|
(fbn--frontmatter-field "title" buffer))
|
||||||
|
|
||||||
|
(defun fbn--set-note-id (id &optional buffer)
|
||||||
|
"Set the specified ID into the frontmatter in BUFFER.
|
||||||
|
|
||||||
|
This replaces the existing ID, if present, otherwise it adds it."
|
||||||
|
(let ((in-buffer (or buffer (current-buffer))))
|
||||||
|
(with-current-buffer in-buffer
|
||||||
|
(let ((existing-id))
|
||||||
|
(setq existing-id (fbn--note-id buffer))
|
||||||
|
(unless (equal id existing-id)
|
||||||
|
(if existing-id
|
||||||
|
(delete-region (line-beginning-position)
|
||||||
|
(progn (forward-line 1) (point))))
|
||||||
|
(fbn--frontmatter-end buffer)
|
||||||
|
(insert (concat "fbnote: " id "\n"))
|
||||||
|
)))))
|
||||||
|
|
||||||
|
(defun fbnote-publish-markdown (&optional buffer)
|
||||||
|
"Publish the current markdown BUFFER as a facebook note."
|
||||||
|
(interactive)
|
||||||
|
(let ((in-buffer (or buffer (current-buffer))))
|
||||||
|
(save-excursion
|
||||||
|
(with-current-buffer in-buffer
|
||||||
|
(let ((note-id (fbn--note-id in-buffer))
|
||||||
|
(note-title (fbn--note-title in-buffer))
|
||||||
|
(note-html (fbn--convert in-buffer))
|
||||||
|
(new-id))
|
||||||
|
(setq new-id (fbn--publish note-title note-id note-html))
|
||||||
|
(fbn--set-note-id new-id)
|
||||||
|
(message (concat "Published note " new-id)))))))
|
||||||
|
|
||||||
|
(provide 'fb-note-publish)
|
||||||
|
;;; fb-note-publish.el ends here
|
||||||
BIN
site-lisp/fb-note-publish.elc
Normal file
BIN
site-lisp/fb-note-publish.elc
Normal file
Binary file not shown.
66
site-lisp/ox-quip.el
Normal file
66
site-lisp/ox-quip.el
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
;;; ox-quip.el -- Publish from org-mode to Quip.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
;; Publisher from org-mode to Quip. (Export as markdown, push as a new
|
||||||
|
;; thread or amend to existing quip thread.)
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
(require 'cl-extra)
|
||||||
|
(require 'ox-md)
|
||||||
|
(require 'quip)
|
||||||
|
|
||||||
|
|
||||||
|
(org-export-define-derived-backend 'quip-html 'html
|
||||||
|
:options-alist
|
||||||
|
'((:section-numbers nil "num" nil)
|
||||||
|
(:with-toc nil "toc" nil))
|
||||||
|
|
||||||
|
|
||||||
|
(defun org-quip--get-thread-identifier ()
|
||||||
|
"Get the Quip thread identifier from the doc in the current buffer, if any."
|
||||||
|
(org-entry-get nil "quip-id" t))
|
||||||
|
|
||||||
|
(defun org-quip--put-thread-identifier (identifier)
|
||||||
|
"Put the Quip thread identifier in IDENTIFIER into the doc."
|
||||||
|
(save-excursion
|
||||||
|
(while (org-up-heading-safe))
|
||||||
|
(org-entry-put nil "quip-id" identifier)))
|
||||||
|
|
||||||
|
(defun org-quip--publish-quip (content)
|
||||||
|
"Publish CONTENT as a new Quip document. Return the ID of the new document."
|
||||||
|
(let ((response (quip-new-document content)))
|
||||||
|
(cdr (assoc 'id (cdr (assoc 'thread response))))))
|
||||||
|
|
||||||
|
(defun org-quip-publish-to-quip ()
|
||||||
|
"Publish the current buffer to Quip."
|
||||||
|
(interactive)
|
||||||
|
(let
|
||||||
|
((quip-id (org-quip--get-thread-identifier))
|
||||||
|
(content (org-export-as 'md)))
|
||||||
|
(if quip-id
|
||||||
|
(org-quip-update-quip quip-id content)
|
||||||
|
(let ((new-quip-id (org-quip--publish-quip content)))
|
||||||
|
(org-quip--put-thread-identifier new-quip-id)))))
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
;; Org-to-quip filter:
|
||||||
|
;;
|
||||||
|
;; So, Quip HTML is a very specific, strict subset of HTML. Quip has only a
|
||||||
|
;; few different block types, and it can't do certain things (like
|
||||||
|
;; multi-paragraph list items.)
|
||||||
|
;;
|
||||||
|
;; Structure:
|
||||||
|
;; - A list of top-level items:
|
||||||
|
;;
|
||||||
|
;; Headlines: <h1> <h2> <h3>
|
||||||
|
;; Block quotes: <blockquote>
|
||||||
|
;; Code block: <pre>
|
||||||
|
;;
|
||||||
|
;; <h3>
|
||||||
|
;;
|
||||||
|
;;
|
||||||
|
|
||||||
|
|
||||||
|
(provide 'ox-quip)
|
||||||
|
;;; ox-quip.el ends here
|
||||||
228
site-lisp/quip.el
Normal file
228
site-lisp/quip.el
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
;;; quip.el --- Quip API client for emacs -*- lexical-binding: t; -*-
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'cl-lib)
|
||||||
|
(require 'json)
|
||||||
|
(require 'url)
|
||||||
|
|
||||||
|
|
||||||
|
;; Customization groups
|
||||||
|
(defgroup quip-api nil
|
||||||
|
"Customization options for using the Quip API"
|
||||||
|
:prefix "quip-"
|
||||||
|
:group 'external
|
||||||
|
:tag "Quip")
|
||||||
|
|
||||||
|
(defcustom quip-api-key ""
|
||||||
|
"Your API key for Quip.
|
||||||
|
|
||||||
|
Get it from https://fb.quip.com/api/personal-token."
|
||||||
|
:type 'string
|
||||||
|
:group 'quip-api)
|
||||||
|
|
||||||
|
(defun quip-invoke-json (path method params)
|
||||||
|
"Make a request to the Quip API, and return the parsed JSON from the response.
|
||||||
|
|
||||||
|
A Quip API call involves issuing an HTTP request to path PATH,
|
||||||
|
with method METHOD, and parameters PARAMS. This routine knows the
|
||||||
|
base URL and adds the necessary headers."
|
||||||
|
(if (not quip-api-key)
|
||||||
|
(error "%s"
|
||||||
|
"The custom variable quip-api-key is undefined. Use custom-set-variable to set it before using quip."))
|
||||||
|
(let
|
||||||
|
((url (concat "https://platform.quip.com/1/" path))
|
||||||
|
(url-request-method method)
|
||||||
|
(url-request-extra-headers `(("Authorization" . ,(concat "Bearer " quip-api-key))
|
||||||
|
("Content-Type" . "application/x-www-form-urlencoded")))
|
||||||
|
(url-request-data
|
||||||
|
(mapconcat (lambda (pair) (format "%s=%s" (car pair) (url-hexify-string (cdr pair))))
|
||||||
|
params
|
||||||
|
"&")))
|
||||||
|
(with-current-buffer (url-retrieve-synchronously url)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(re-search-forward "^$")
|
||||||
|
(json-read))))
|
||||||
|
|
||||||
|
(defun quip-new-document (content &optional format)
|
||||||
|
"Create a new Quip document with the provided CONTENT.
|
||||||
|
|
||||||
|
This function returns the parsed JSON response. The optional
|
||||||
|
FORMAT argument is one of 'html' or 'markdown', and indicates
|
||||||
|
that the content should be interpreted as such."
|
||||||
|
(quip-invoke-json "threads/new-document"
|
||||||
|
"POST"
|
||||||
|
`((format . ,(or format "markdown"))
|
||||||
|
(content . ,content))))
|
||||||
|
|
||||||
|
(defun quip-get-thread (id)
|
||||||
|
"Get the Quip thread with the specified ID. Return the parsed JSON response."
|
||||||
|
(quip-invoke-json (concat "threads/" id) "GET" nil))
|
||||||
|
|
||||||
|
(defconst quip-location-append 0)
|
||||||
|
(defconst quip-location-prepend 1)
|
||||||
|
(defconst quip-location-after-section 2)
|
||||||
|
(defconst quip-location-before-section 3)
|
||||||
|
(defconst quip-location-replace-section 4)
|
||||||
|
(defconst quip-location-delete-section 5)
|
||||||
|
|
||||||
|
(defun quip-thread-append (thread content &optional format)
|
||||||
|
; checkdoc-order: nil
|
||||||
|
"Append CONTENT to the specified THREAD.
|
||||||
|
|
||||||
|
The optional FORMAT argument is one of 'html' or 'markdown', and
|
||||||
|
indicates how the content is to be interpreted."
|
||||||
|
(quip-invoke-json "threads/edit-document"
|
||||||
|
"POST"
|
||||||
|
`((format . ,(or format "markdown"))
|
||||||
|
(content . ,content)
|
||||||
|
(location . ,quip-location-append)
|
||||||
|
(thread_id . ,thread))))
|
||||||
|
|
||||||
|
(defun quip-thread-prepend (thread content &optional format)
|
||||||
|
; checkdoc-order: nil
|
||||||
|
"Prepend CONTENT to the specified THREAD.
|
||||||
|
|
||||||
|
The optional FORMAT argument is one of 'html' or 'markdown', and
|
||||||
|
indicates how the content is to be interpreted."
|
||||||
|
(quip-invoke-json "threads/edit-document"
|
||||||
|
"POST"
|
||||||
|
`((format . ,(or format "markdown"))
|
||||||
|
(content . ,content)
|
||||||
|
(location . ,quip-location-prepend)
|
||||||
|
(thread_id . ,thread))))
|
||||||
|
|
||||||
|
(defun quip-thread-append-after (thread section content &optional format)
|
||||||
|
; checkdoc-order: nil
|
||||||
|
"Append CONTENT to specified SECTION in the specified THREAD.
|
||||||
|
|
||||||
|
The content is appended after the specified section.
|
||||||
|
|
||||||
|
The optional FORMAT argument is one of 'html' or 'markdown', and
|
||||||
|
indicates how the content is to be interpreted."
|
||||||
|
(quip-invoke-json "threads/edit-document"
|
||||||
|
"POST"
|
||||||
|
`((format . ,(or format "markdown"))
|
||||||
|
(content . ,content)
|
||||||
|
(location . ,quip-location-after-section)
|
||||||
|
(section_id . ,section)
|
||||||
|
(thread_id . ,thread))))
|
||||||
|
|
||||||
|
(defun quip-thread-prepend-before (thread section content &optional format)
|
||||||
|
; checkdoc-order: nil
|
||||||
|
"Prepend CONTENT to the specified SECTION of THREAD.
|
||||||
|
|
||||||
|
The content is added before the specified section.
|
||||||
|
|
||||||
|
The optional FORMAT argument is one of 'html' or 'markdown', and
|
||||||
|
indicates how the content is to be interpreted."
|
||||||
|
(quip-invoke-json "threads/edit-document"
|
||||||
|
"POST"
|
||||||
|
`((format . ,(or format "markdown"))
|
||||||
|
(content . ,content)
|
||||||
|
(location . ,quip-location-before-section)
|
||||||
|
(section_id . ,section)
|
||||||
|
(thread_id . ,thread))))
|
||||||
|
|
||||||
|
(defun quip-thread-replace-section (thread section content &optional format)
|
||||||
|
; checkdoc-order: nil
|
||||||
|
"Replace the specified SECTION of THREAD with the specified CONTENT.
|
||||||
|
|
||||||
|
The optional FORMAT argument is one of 'html' or 'markdown', and
|
||||||
|
indicates how the content is to be interpreted."
|
||||||
|
(quip-invoke-json "threads/edit-document"
|
||||||
|
"POST"
|
||||||
|
`((format . ,(or format "markdown"))
|
||||||
|
(content . ,content)
|
||||||
|
(location . ,quip-location-replace-section)
|
||||||
|
(section_id . ,section)
|
||||||
|
(thread_id . ,thread))))
|
||||||
|
|
||||||
|
(defun quip-thread-delete-section (thread section)
|
||||||
|
; checkdoc-order: nil
|
||||||
|
"Delete the specified SECTION of THREAD."
|
||||||
|
(quip-invoke-json "threads/edit-document"
|
||||||
|
"POST"
|
||||||
|
`((location . ,quip-location-delete-section)
|
||||||
|
(section_id . ,section)
|
||||||
|
(thread_id . ,thread))))
|
||||||
|
|
||||||
|
|
||||||
|
;;; Content parsing functions
|
||||||
|
|
||||||
|
(defun quip-get-item-type (item)
|
||||||
|
"Classify the specified HTML ITEM."
|
||||||
|
(let ((elem-type (car item)))
|
||||||
|
(cond
|
||||||
|
((eq elem-type 'p) 'paragraph)
|
||||||
|
((eq elem-type 'h1) 'h1)
|
||||||
|
((eq elem-type 'h2) 'h2)
|
||||||
|
((eq elem-type 'h3) 'h3)
|
||||||
|
((eq elem-type 'blockquote) 'block-quote)
|
||||||
|
((eq elem-type 'q) 'pull-quote)
|
||||||
|
((eq elem-type 'pre) 'code-block)
|
||||||
|
((eq elem-type 'li) 'list-item)
|
||||||
|
((eq elem-type 'span) 'span)
|
||||||
|
((eq elem-type 'div)
|
||||||
|
(letrec ((inner (cl-caddr item))
|
||||||
|
(inner-elem-type (car inner)))
|
||||||
|
(cond
|
||||||
|
((eq inner-elem-type 'ul) 'ul)
|
||||||
|
((eq inner-elem-type 'ol) 'ol)
|
||||||
|
(t 'unrecognized-inner))))
|
||||||
|
(t 'unrecognized))))
|
||||||
|
|
||||||
|
(defun quip-get-item-id (item type)
|
||||||
|
"Extract the ID from the provided ITEM given its TYPE."
|
||||||
|
(let ((attribs (cadr item)))
|
||||||
|
(cond
|
||||||
|
((or (eq type 'ul) ;; Nested IDs.
|
||||||
|
(eq type 'ol))
|
||||||
|
(letrec ((inner (cl-caddr item))
|
||||||
|
(inner-attribs (cadr inner)))
|
||||||
|
(assoc-default 'id inner-attribs)))
|
||||||
|
(t (assoc-default 'id attribs)))))
|
||||||
|
|
||||||
|
(defun quip-get-item-content (item type)
|
||||||
|
"Extract the content from the provided ITEM given its TYPE."
|
||||||
|
(cond
|
||||||
|
((or (eq type 'ul) ;; Nested Content
|
||||||
|
(eq type 'ol))
|
||||||
|
(letrec ((inner (cl-caddr item))
|
||||||
|
(inner-elems (cddr inner)))
|
||||||
|
(mapcar #'quip-get-item-from-element inner-elems)))
|
||||||
|
(t (cl-caddr item))))
|
||||||
|
|
||||||
|
(cl-defstruct quip-item type id content)
|
||||||
|
|
||||||
|
(defun quip-get-item-from-element (element)
|
||||||
|
"Construct a (type, id, content) list from the given ELEMENT."
|
||||||
|
(letrec
|
||||||
|
((item-type (quip-get-item-type element))
|
||||||
|
(item-id (quip-get-item-id element item-type))
|
||||||
|
(item-content (quip-get-item-content element item-type)))
|
||||||
|
(make-quip-item
|
||||||
|
:type item-type
|
||||||
|
:id item-id
|
||||||
|
:content item-content)))
|
||||||
|
|
||||||
|
|
||||||
|
(defun quip-parse-html-content (html)
|
||||||
|
"Parse the provided HTML into a list of (type, item, content) lists."
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert html)
|
||||||
|
(letrec
|
||||||
|
((parsed-html (libxml-parse-html-region (point-min) (point-max)))
|
||||||
|
(raw-items (cddr (cl-caddr parsed-html)))
|
||||||
|
(html-items (cl-remove-if #'stringp raw-items)))
|
||||||
|
|
||||||
|
(mapcar #'quip-get-item-from-element html-items)
|
||||||
|
)))
|
||||||
|
|
||||||
|
;; (prin1
|
||||||
|
;; (quip-parse-html-content
|
||||||
|
;; (assoc-default 'html (quip-get-thread "idflAWG6R6Uu"))))
|
||||||
|
|
||||||
|
(provide 'quip)
|
||||||
|
;;; quip.el ends here
|
||||||
Loading…
Add table
Add a link
Reference in a new issue