diff --git a/Eldev b/Eldev index 9758193..a7c0ac0 100644 --- a/Eldev +++ b/Eldev @@ -3,4 +3,4 @@ ;; Uncomment some calls below as needed for your project. ;(eldev-use-package-archive 'gnu) ;(eldev-use-package-archive 'nongnu) -;(eldev-use-package-archive 'melpa) +(eldev-use-package-archive 'melpa) diff --git a/lotion-api.el b/lotion-api.el new file mode 100644 index 0000000..476e46a --- /dev/null +++ b/lotion-api.el @@ -0,0 +1,82 @@ +;;; lotion-api.el --- Lotion api -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 + +;; URL: https://github.com/sienic/lotion +;; Version: 0.1.0 +;; Keywords: org-mode, notion +;; Package-Requires: ((emacs "26.1") (org "9.4") + +;; This file is NOT part of GNU Emacs. + +;; This program 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. + +;; This program 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 this program. If not, see . +;; +;;; Commentary: +;; +;; Lotion api +;; +;;; Code: + +(defun lotion-api-page-fetch (uuid &optional callback) + (lotion-api--get (format "/v1/pages/%s" uuid) callback)) + +(defun lotion-api-blocks-fetch (uuid &optional callback) + (lotion-api--get (format "/v1/blocks/%s/children" uuid) callback)) + +(defun lotion-api-block-patch (uuid payload &optional callback) + (lotion-api--patch (format "/v1/blocks/%s" uuid) payload callback)) + +(defun lotion-api--get (path &optional callback) + (lotion-api--request + :path path + :callback callback + :payload '(("page_size . 100")))) + +(defun lotion-api--patch (path payload &optional callback) + (lotion-api--request + :path path + :method "PATCH" + :callback callback + :payload (json-encode payload) + :headers '(("Content-Type" . "application/json")))) + +(cl-defun lotion-api--request (&key path method callback payload headers) + "Performs a request to notion" + (let ((type (or method "GET"))) + (request (concat "https://" lotion-default-host path) + :type type + :parser 'json-read + :data payload + :headers (append headers `(("Notion-Version" . "2022-02-22") + ("Authorization" . ,(concat "Bearer " (lotion-token))))) + :success (cl-function + (lambda (&key data &allow-other-keys) + (if callback (funcall callback data nil)) + (setq my/data data))) + :error (cl-function (lambda (&rest args &key error-thrown &allow-other-keys) + (if callback (funcall callback nil error-thrown)) + (message "Got error: %S" error-thrown)))))) + +(defun alist-get-in (alist symbols) + "Navigate an ALIST via SYMBOLS. +Numbers in SYMBOLS are considered indeces of sequences." + (if symbols + (if-let ((index (and (numberp (car symbols)) + (car symbols)))) + (alist-get-in (nth index (append alist nil)) (cdr symbols)) + (alist-get-in (alist-get (car symbols) alist) (cdr symbols))) + alist)) + +(provide 'lotion-api) +;;; lotion-api.el ends here diff --git a/lotion-oom.el b/lotion-oom.el new file mode 100644 index 0000000..c61f441 --- /dev/null +++ b/lotion-oom.el @@ -0,0 +1,60 @@ +;;; lotion-oom.el --- Lotion org object model -*- lexical-binding: t; -*- + +;; +;; Copyright (C) 2022 + +;; URL: https://github.com/sienic/lotion +;; Version: 0.1.0 +;; Keywords: org-mode, notion +;; Package-Requires: ((emacs "26.1") (org "9.4") + +;; This file is NOT part of GNU Emacs. + +;; This program 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. + +;; This program 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 this program. If not, see . +;; +;;; Commentary: +;; +;; Lotion oom +;; +;;; Code: + +(defconst lotion-oom-org-template-lookup + '((page . "* %s") (heading_1 . "** %s") (heading_2 . "*** %s") (paragraph . "%s")) + "Map of org templates per block type") + +(defun lotion-oom--template-get (block-type) + "Returns the template for the BLOCK-TYPE" + (cdr (assq block-type lotion-oom-org-template-lookup))) + +(defun lotion-oom--page--to-org (page) + "Convert PAGE into an org line" + (format (lotion-oom--template-get (page-type page)) + (page-title page))) + +(defun lotion-oom--block-to-org (block) + "Convert BLOCK into an org line" + (format (lotion-oom--template-get (block-type block)) + (block-content block))) + +(defun lotion-oom--element-to-org (elt) + "Convert an ELT into an org line" + (cond ((page-p elt) (lotion-oom--page--to-org elt)) + ((block-p elt) (lotion-oom--block-to-org elt)))) + +(defun lotion-oom-page-to-org (page) + "Converts a page and its children blocks into org syntax" + (string-join + (mapcar #'lotion-oom--element-to-org (cons page (page-blocks page))) "\n")) + +(provide 'lotion-oom) diff --git a/lotion-parse.el b/lotion-parse.el new file mode 100644 index 0000000..f445238 --- /dev/null +++ b/lotion-parse.el @@ -0,0 +1,56 @@ +;;; lotion-parse.el --- Lotion parse -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 + +;; URL: https://github.com/sienic/lotion +;; Version: 0.1.0 +;; Keywords: org-mode, notion +;; Package-Requires: ((emacs "26.1") (org "9.4") + +;; This file is NOT part of GNU Emacs. + +;; This program 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. + +;; This program 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 this program. If not, see . +;; +;;; Commentary: +;; +;; Lotion parser +;; +;;; Code: + +;; data models +;; block is a object-type block +(cl-defstruct block id type content-type content) +;; a page is a type of block (let's store it separate for now) +(cl-defstruct page id type title blocks) + +(defun lotion-parse-page (page-data blocks-data) + "Parses a PAGE-DATA and BLOCKS-DATA from Notion's API into Lotion models" + (make-page :id (alist-get-in page-data '(id)) + :type (intern (alist-get-in page-data '(object))) + :title (alist-get-in page-data '(properties Name title 0 plain_text)) + :blocks (lotion-parse--blocks blocks-data))) + +(defun lotion-parse--blocks (data) + (let ((blocks (alist-get-in data '(results)))) + (mapcar #'lotion-parse--block blocks))) + +(defun lotion-parse--block (data) + (let* ((id (alist-get-in data '(id))) + (type (intern (alist-get-in data '(type)))) + (content-type (intern (alist-get-in data `(,type rich_text 0 type)))) + (content (alist-get-in data `(,type rich_text 0 ,content-type content)))) + (make-block :id id :type type :content-type content-type :content content))) + +(provide 'lotion-parse) +;;; lotion-parse.el ends here diff --git a/lotion-render.el b/lotion-render.el new file mode 100644 index 0000000..961360d --- /dev/null +++ b/lotion-render.el @@ -0,0 +1,46 @@ +;;; lotion-render.el --- Lotion render -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2022 + +;; URL: https://github.com/sienic/lotion +;; Version: 0.1.0 +;; Keywords: org-mode, notion +;; Package-Requires: ((emacs "26.1") (org "9.4") + +;; This file is NOT part of GNU Emacs. + +;; This program 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. + +;; This program 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 this program. If not, see . +;; +;;; Commentary: +;; +;; Lotion render +;; +;;; Code: + +(defun lotion-render-page (page) + "Render PAGE and its contents" + (lotion-render--flush (lotion-oom-page-to-org page))) + +(defun lotion-render--flush(content) + "Flushes CONTENT into the lotion buffer" + (save-excursion + (with-current-buffer (get-buffer-create "*lotion*") + (org-mode) + (save-excursion + (delete-region (point-min) (point-max)) + (insert content))) + (switch-to-buffer-other-window "*lotion*"))) + +(provide 'lotion-render) +;;; lotion-render.el ends here diff --git a/lotion.el b/lotion.el index 03adf50..cef10aa 100644 --- a/lotion.el +++ b/lotion.el @@ -4,8 +4,8 @@ ;; URL: https://github.com/sienic/lotion ;; Version: 0.1.0 -;; Keywords: org-mode, notion -;; Package-Requires: ((emacs "26.1") (org "9.4") +;; Keywords: org-mode +;; Package-Requires: ((emacs "26.1") (request "0.3.3")) ;; This file is NOT part of GNU Emacs. @@ -27,10 +27,13 @@ ;; Lotion is an attempt to use Emacs and Notion simultaneously, by being able to ;; import and export content from Notion, while keeping data consistency. ;; - ;;; Code: -(require 'org) -(require 'request) +(require 'org) ; +(require 'request) ; +(require 'lotion-api) ; communication layer with Notion +(require 'lotion-parse) ; transforms notion data into lotion data structures +(require 'lotion-oom) ; creates org raw text nodes from pages and blocks +(require 'lotion-render) ; renders text representing org elements ;;; Options (defgroup lotion nil @@ -55,136 +58,34 @@ "Get token to query Notion API." (or lotion-token (auth-source-pick-first-password :host lotion-default-host :user lotion-user))) -(cl-defun lotion-request (&key path method callback payload headers) - "Performs a request to notion" - (let ((type (or method "GET"))) - (request (concat "https://" lotion-default-host path) - :type type - :parser 'json-read - :data payload - :headers (append headers `(("Notion-Version" . "2022-02-22") - ("Authorization" . ,(concat "Bearer " (lotion-token))))) - :success (cl-function - (lambda (&key data &allow-other-keys) - (if callback (funcall callback data nil)) - (setq my/data data))) - :error (cl-function (lambda (&rest args &key error-thrown &allow-other-keys) - (if callback (funcall callback nil error-thrown)) - (message "Got error: %S" error-thrown)))))) - -(defun lotion-request--get (path &optional callback) - (lotion-request - :path path - :callback callback - :payload '(("page_size . 100")))) - -(defun lotion-request--patch (path payload &optional callback) - (lotion-request - :path path - :method "PATCH" - :callback callback - :payload (json-encode payload) - :headers '(("Content-Type" . "application/json")))) - -;; api resources -(defun lotion-page--fetch (uuid &optional callback) - (lotion-request--get (format "/v1/pages/%s" uuid) callback)) - -(defun lotion-blocks--fetch (uuid &optional callback) - (lotion-request--get (format "/v1/blocks/%s/children" uuid) callback)) - -(defun lotion-blocks--patch (uuid payload &optional callback) - (lotion-request--patch (format "/v1/blocks/%s" uuid) payload callback)) - -(defun alist-get-in (alist symbols) - "Navigate an ALIST via SYMBOLS. -Numbers in SYMBOLS are considered indeces of sequences." - (if symbols - (if-let ((index (and (numberp (car symbols)) - (car symbols)))) - (alist-get-in (nth index (append alist nil)) (cdr symbols)) - (alist-get-in (alist-get (car symbols) alist) (cdr symbols))) - alist)) - +;;; playground ;; example data (setq page-data nil) (setq blocks-data nil) ;; blocks-data ; => ((object . "list") (results . [((object . "block") (id . "ee245fe3-b41c-4d31-9033-70795fd09ba8") (created_time . "2022-04-07T17:50:00.000Z") (last_edited_time . "2022-04-07T17:50:00.000Z") (created_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (last_edited_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (has_children . :json-false) (archived . :json-false) (type . "heading_1") (heading_1 (rich_text . [((type . "text") (text (content . "Header 1") (link)) (annotations (bold . :json-false) (italic . :json-false) (strikethrough . :json-false) (underline . :json-false) (code . :json-false) (color . "default")) (plain_text . "Header 1") (href))]) (color . "default"))) ((object . "block") (id . "18ceecee-ecd0-48e2-95d3-21dd9742ea9d") (created_time . "2022-04-07T17:50:00.000Z") (last_edited_time . "2022-04-07T17:50:00.000Z") (created_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (last_edited_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (has_children . :json-false) (archived . :json-false) (type . "paragraph") (paragraph (rich_text . [((type . "text") (text (content . "Text") (link)) (annotations (bold . :json-false) (italic . :json-false) (strikethrough . :json-false) (underline . :json-false) (code . :json-false) (color . "default")) (plain_text . "Text") (href))]) (color . "default"))) ((object . "block") (id . "d695bcf2-2124-413d-9f15-b01144040e3a") (created_time . "2022-04-07T17:50:00.000Z") (last_edited_time . "2022-04-07T17:50:00.000Z") (created_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (last_edited_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (has_children . :json-false) (archived . :json-false) (type . "heading_2") (heading_2 (rich_text . [((type . "text") (text (content . "Header 2") (link)) (annotations (bold . :json-false) (italic . :json-false) (strikethrough . :json-false) (underline . :json-false) (code . :json-false) (color . "default")) (plain_text . "Header 2") (href))]) (color . "default"))) ((object . "block") (id . "d7e66203-049b-4dd6-ad3d-76076ba35b47") (created_time . "2022-04-07T17:50:00.000Z") (last_edited_time . "2022-04-07T17:50:00.000Z") (created_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (last_edited_by (object . "user") (id . "e8fb2099-0bfe-41e9-a688-9e9ca053464d")) (has_children . :json-false) (archived . :json-false) (type . "paragraph") (paragraph (rich_text . [((type . "text") (text (content . "Another text") (link)) (annotations (bold . :json-false) (italic . :json-false) (strikethrough . :json-false) (underline . :json-false) (code . :json-false) (color . "default")) (plain_text . "Another text") (href))]) (color . "default")))]) (next_cursor) (has_more . :json-false) (type . "block") (block)) -(lotion-page--fetch "201392c852284d7c8020d2d6421a9e58" (lambda (data err) (setq page-data data))) +(lotion-api-page-fetch "201392c852284d7c8020d2d6421a9e58" (lambda (data err) (setq page-data data))) ;; (alist-get-in page-data '(properties Name title 0 plain_text)) ; => "Getting a normal card with simple text nodes" -(lotion-blocks--fetch "201392c852284d7c8020d2d6421a9e58" (lambda (data err) (setq blocks-data data))) +(lotion-api-blocks-fetch "201392c852284d7c8020d2d6421a9e58" (lambda (data err) (setq blocks-data data))) ;; (alist-get-in my/data '(results 0 heading_1 rich_text 0 plain_text)) ; => "Header 1" ;; (alist-get-in my/data '(results 1 paragraph rich_text 0 plain_text)) ; => "Text" ;; (alist-get-in my/data '(results 2 heading_2 rich_text 0 plain_text)) ; => "Header 2" ;; (alist-get-in my/data '(results 3 paragraph rich_text 0 plain_text)) ; => "Another text" -;; data models -(cl-defstruct block id type content-type content) -(cl-defstruct page id title blocks) - -;; mappers from lotion responses to data models -(defun parse-block (data) - (let* ((id (alist-get-in data '(id))) - (type (intern (alist-get-in data '(type)))) - (content-type (intern (alist-get-in data `(,type rich_text 0 type)))) - (content (alist-get-in data `(,type rich_text 0 ,content-type content)))) - (make-block :id id :type type :content-type content-type :content content))) - -(defun parse-blocks (data) - (let ((blocks (alist-get-in data '(results)))) - (mapcar #'parse-block blocks))) - -;; (parse-blocks blocks-data) - -(defun parse-page (page-data blocks-data) - (make-page :id (alist-get-in page-data '(id)) - :title (alist-get-in page-data '(properties Name title 0 plain_text)) - :blocks (parse-blocks blocks-data))) - -;; (parse-page page-data blocks-data) - -;; view functions -(setq types-prefix '((heading_1 . "**") (heading_2 . "***") (paragraph . nil))) - -(defun block-heading (type) - (cdr (assoc type types-prefix))) - -(defun block-to-org (block) - (string-join - (remq nil `(,(block-heading (block-type block)) - ,(block-content block))) - " ")) - -(defun page-to-org (page) - (string-join - `(,(string-join `("*" ,(page-title page)) " ") - ,@(mapcar #'block-to-org (page-blocks page))) "\n")) - -;; rendering functions -(defun render-page (page) - (lotion-write-into-buffer (page-to-org page))) - -(defun lotion-write-into-buffer (header) - (save-excursion - (let ((lotion-buffer (get-buffer-create "*lotion*"))) - (with-current-buffer lotion-buffer - (org-mode) - (insert header))))) - -;; function to dispatch retrieval and rendering -(defun lotion-page (uuid) +(defun lotion-page-get (page-uuid) + "Fetch and render the page with PAGE-UUID and its children" (let ((page nil)) (message "executing") - (lotion-page--fetch - uuid + (lotion-api-page-fetch + page-uuid (lambda (page-data err) - (lotion-blocks--fetch - uuid + (lotion-api-blocks-fetch + page-uuid (lambda (blocks-data err) - (render-page (parse-page page-data blocks-data)))))))) + (lotion-render-page + (lotion-parse-page page-data blocks-data)))))))) ;; fetch and store content in *lotion* buffer -(lotion-page "201392c852284d7c8020d2d6421a9e58") +(lotion-page-get "201392c852284d7c8020d2d6421a9e58") ;; updating notion from a model (setq block-to-update @@ -192,10 +93,12 @@ Numbers in SYMBOLS are considered indeces of sequences." :type "heading_1" :content-type "text" :content "New text")) + +;; TODO: Figure out in which layer this should live (defun block-to-payload (block) `((,(block-type block) ("rich_text" ((,(block-content-type block) ("content" . ,(block-content block)))))))) -(lotion-blocks--patch +(lotion-api-block-patch "ee245fe3-b41c-4d31-9033-70795fd09ba8" (block-to-payload block-to-update) (lambda (data err) (message "xxxxxxxxx %s" data)))