feat: add initial impl with page fetch/edit/push
This commit is contained in:
parent
6890157906
commit
2ddc73e9d9
155
bookstack.el
Normal file
155
bookstack.el
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
;;; bookstack.el --- Remote editing for the BookStack documentation platform -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2022-2023 Alex Smith
|
||||||
|
;; Authors: Alex Smith
|
||||||
|
;; URL: https://git.xeal.me/xeals/bookstack.el
|
||||||
|
;; Created: Tue Oct 11 2022
|
||||||
|
;; Keywrods: comm hypermedia wp
|
||||||
|
;; Version: 0.1.0
|
||||||
|
;; Package-Requires: ((emacs "28.1") markdown-mode)
|
||||||
|
|
||||||
|
;; This file is not part of GNU Emacs.
|
||||||
|
|
||||||
|
;; GNU Emacs is free software: you can redistribute it and/or modify
|
||||||
|
;; it under the terms of the GNU General Public License as published by
|
||||||
|
;; the Free Software Foundation, either version 3 of the License, or
|
||||||
|
;; (at your option) any later version.
|
||||||
|
|
||||||
|
;; GNU Emacs is distributed in the hope that it will be useful,
|
||||||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
;; GNU General Public License for more details.
|
||||||
|
|
||||||
|
;; You should have received a copy of the GNU General Public License
|
||||||
|
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Provides an interface to BookStack [1], an open-source documentation
|
||||||
|
;; and wiki platform.
|
||||||
|
;;
|
||||||
|
;; In this early iteration, basic editing of pages is supported.
|
||||||
|
;;
|
||||||
|
;; [1]: https://www.bookstackapp.com/
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
;; Emacs 28.1 is required for string processing. These could be polyfilled if
|
||||||
|
;; interest arises.
|
||||||
|
(require 'markdown-mode)
|
||||||
|
(require 'json)
|
||||||
|
(require 'url-vars)
|
||||||
|
|
||||||
|
(defgroup bookstack nil
|
||||||
|
"Utilities to work with a remote BookStack server."
|
||||||
|
:prefix "bookstack-"
|
||||||
|
:group 'hypermedia
|
||||||
|
:group 'wp
|
||||||
|
:link '(url-link :tag "Gitea" "https://git.xeal.me/xeals/bookstack.el")
|
||||||
|
:link '(emacs-commentary-link :tag "Commentary" "bookstack-mode"))
|
||||||
|
|
||||||
|
(defvar bookstack-token nil
|
||||||
|
"BookStack API authentication token.")
|
||||||
|
|
||||||
|
(defvar bookstack-server nil
|
||||||
|
"BookStack server address, including protocol and port (if non-default).")
|
||||||
|
|
||||||
|
(defvar-local bookstack--buffer-page-id nil
|
||||||
|
"ID of the page being edited in the current buffer.")
|
||||||
|
|
||||||
|
(defun bookstack--retrieve (endpoint callback)
|
||||||
|
"Retrieves the ENDPOINT from the current `bookstack-server' asynchronously,
|
||||||
|
executing CALLBACK with the decoded result."
|
||||||
|
(unless bookstack-token
|
||||||
|
(user-error "%s" "`bookstack-token' must be defined"))
|
||||||
|
(unless bookstack-server
|
||||||
|
(user-error "%s" "`bookstack-server' must be defined"))
|
||||||
|
(let ((url-request-extra-headers
|
||||||
|
`(("Authorization" . ,(format "Token %s" bookstack-token))
|
||||||
|
("Content-Type" . "application/json")))
|
||||||
|
(cb
|
||||||
|
(lambda (status)
|
||||||
|
(pcase status
|
||||||
|
(`(:error (,type . ,data)) (user-error "BookStack %s error: %s" type data))
|
||||||
|
(_ (let* ((raw (car (last (string-lines (buffer-string)))))
|
||||||
|
(json (json-read-from-string raw)))
|
||||||
|
(apply callback (list json))))))))
|
||||||
|
(url-retrieve (concat bookstack-server "/api/" endpoint) cb)))
|
||||||
|
|
||||||
|
(defun bookstack--write-page (id content)
|
||||||
|
"Writes CONTENT to the remote page with ID."
|
||||||
|
(let ((url-request-method "PUT")
|
||||||
|
(url-request-data (encode-coding-string
|
||||||
|
(json-encode `((markdown . ,content)))
|
||||||
|
'utf-8)))
|
||||||
|
(bookstack--retrieve (format "pages/%d" id) (lambda (_)))))
|
||||||
|
|
||||||
|
(defun bookstack--read-page (pages)
|
||||||
|
"Reads a page from the user with `completing-read', returning its ID.
|
||||||
|
PAGES should be a sequence of pages (as returned by `bookstack-pages')."
|
||||||
|
(let* ((opts (seq-reduce
|
||||||
|
(lambda (acc page)
|
||||||
|
(push `(,(alist-get 'name page) . ,(alist-get 'id page)) acc))
|
||||||
|
pages
|
||||||
|
nil))
|
||||||
|
(page-name (progn
|
||||||
|
(switch-to-minibuffer)
|
||||||
|
(completing-read "Page: " opts))))
|
||||||
|
(cdr (assoc page-name opts))))
|
||||||
|
|
||||||
|
(defun bookstack--edit-page (id)
|
||||||
|
"Asynchronously loads a page by ID for editing into a new buffer."
|
||||||
|
(bookstack-page
|
||||||
|
id
|
||||||
|
(lambda (page)
|
||||||
|
(or page (error "Missing full page '%d'" id))
|
||||||
|
(let-alist page
|
||||||
|
(let ((buffer (get-buffer-create (format "%s.md" .name)))
|
||||||
|
;; fix line endings
|
||||||
|
(content (string-replace "
" "" .markdown)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(markdown-mode)
|
||||||
|
(setq bookstack--buffer-page-id id)
|
||||||
|
(insert content)
|
||||||
|
(goto-char (point-min)))
|
||||||
|
(switch-to-buffer buffer)
|
||||||
|
(bookstack-mode +1))))))
|
||||||
|
|
||||||
|
(defun bookstack-pages (callback)
|
||||||
|
"Retrieve all pages from `bookstack-server'."
|
||||||
|
(bookstack--retrieve
|
||||||
|
"pages"
|
||||||
|
(lambda (res) (apply callback (list (alist-get 'data res))))))
|
||||||
|
|
||||||
|
(defun bookstack-page (id callback)
|
||||||
|
"Retrieve a single page from `bookstack-server'."
|
||||||
|
(bookstack--retrieve
|
||||||
|
(format "pages/%d" id)
|
||||||
|
(lambda (res) (apply callback (list res)))))
|
||||||
|
|
||||||
|
(defun bookstack-save-buffer ()
|
||||||
|
"Pushes the current buffer to the BookStack server."
|
||||||
|
(interactive)
|
||||||
|
(let ((slug (string-replace " " "-" (downcase (file-name-base (buffer-name))))))
|
||||||
|
(bookstack--write-page bookstack--buffer-page-id (buffer-string))
|
||||||
|
(message "Wrote %s/.../page/%s" bookstack-server slug)))
|
||||||
|
|
||||||
|
(define-minor-mode bookstack-mode
|
||||||
|
"Minor mode for editing remote BookStack buffers."
|
||||||
|
:init-value nil
|
||||||
|
:lighter " BookStack"
|
||||||
|
:keymap (let ((map (make-sparse-keymap)))
|
||||||
|
(define-key map [remap save-buffer] #'bookstack-save-buffer)
|
||||||
|
map))
|
||||||
|
|
||||||
|
;;;###autoload
|
||||||
|
(defun bookstack-find-page ()
|
||||||
|
"Prompts the user to find and edit a BookStack page."
|
||||||
|
(interactive)
|
||||||
|
(bookstack-pages
|
||||||
|
(lambda (pages)
|
||||||
|
(bookstack--edit-page (bookstack--read-page pages)))))
|
||||||
|
|
||||||
|
(provide 'bookstack)
|
||||||
|
|
||||||
|
;;; bookstack.el ends here
|
Loading…
Reference in New Issue
Block a user