branch: elpa/gnosis
commit b45a6982d087a8f13ba6b06d5ba8ccf566af9e29
Author: Thanos Apollo <pub...@thanosapollo.org>
Commit: Thanos Apollo <pub...@thanosapollo.org>

    Rewrite tags & extraction of links.
    
    * We are using a new tags table.
    * Extraction of links will occur in parathema.
---
 gnosis.el | 656 +++++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 376 insertions(+), 280 deletions(-)

diff --git a/gnosis.el b/gnosis.el
index 1adf57fac5..ae1a6805e9 100644
--- a/gnosis.el
+++ b/gnosis.el
@@ -1022,7 +1022,7 @@ SUSPEND: whether to suspend not"
   (cl-assert (listp images) nil "Images must be a list of string paths")
   (cl-assert (listp tags) nil "Tags value must be a list of tags as strings")
   (cl-assert (or (= suspend 1) (= suspend 0)) nil "Suspend value must be 
either 0 or 1")
-  (gnosis-add-note-fields deck "mc-cloze" question options answer extra tags 
(or suspend 0)
+  (gnosis-add-thema-fields deck "mc-cloze" question options answer extra tags 
(or suspend 0)
                          (car images) (cdr images)))
 
 (defun gnosis-add-note-mc-cloze (deck)
@@ -1190,81 +1190,60 @@ DATE is a list of the form (year month day)."
          (time-date (encode-time 0 0 0 (nth 2 date) (nth 1 date) (nth 0 
date))))
     (not (time-less-p time-now time-date))))
 
-(cl-defun gnosis-tag-prompt (&key (prompt "Selected tags:") (due nil))
-  "PROMPT user to select tags, until they enter `q'.
-
-Prompt user to select tags, generated from `gnosis-get-tags--unique'.
-PROMPT: Prompt string value
-MATCH: Require match, t or nil value
-DUE: if t, return tags for due notes from `gnosis-due-tags'."
-  (let ((tags '()))
-    (cl-loop for tag = (completing-read
-                       (concat prompt (format " (%s) (q for quit): " 
(mapconcat #'identity tags " ")))
-                       (cons "q" (if due (gnosis-review-get-due-tags)
-                                   (gnosis-get-tags--unique)))
-                       nil t)
-            until (string= tag "q")
-            unless (member tag tags)
-            do (push tag tags))
-    tags))
-
-(defun gnosis-hint-prompt (previous-hint &optional prompt)
-  "Prompt user for hint.
-
-PROMPT: Prompt string value
-PREVIOUS-HINT: Previous hint value, if any.  If nil, use PROMPT as
-default value."
-  (let* ((prompt (or prompt "Hint: "))
-        (hint (read-string prompt previous-hint)))
-    (setf gnosis-previous-note-hint hint)
-    hint))
-
-(defun gnosis-prompt-mcq-input (&optional prompt string)
-  "PROMPT for MCQ note content.
-
-STRING: Guidance string."
-  (let ((user-input (gnosis-read-string-from-buffer (or prompt (car 
gnosis-mcq-guidance) "")
-                                                   (or string (cdr 
gnosis-mcq-guidance) ""))))
-    (cond ((not (string-match-p gnosis-mcq-separator user-input))
-          (gnosis-prompt-mcq-input (format "`gnosis-mcq-separator': %s not 
found!" gnosis-mcq-separator)
-                                   user-input))
-         ((not (string-match "{.*}" user-input))
-          (gnosis-prompt-mcq-input (format "Please wrap the right option with 
{}")
-                                   user-input))
-         (t (gnosis-mcq-process-input user-input)))))
-
-(defun gnosis-mcq-process-input (user-input &optional stem-separator 
option-separator)
-  "Process USER-INPUT for MCQ note.
-
-STEM-SEPARATOR: Separator of question stem & options
-OPTION-SEPARATOR: Separator of each option
-
-Return ((QUESTION CHOICES) CORRECT-CHOICE-INDEX)"
-  (let* ((stem-separator (or stem-separator gnosis-mcq-separator))
-        (option-separator (or option-separator gnosis-mcq-option-separator))
-        (input-separated (split-string user-input stem-separator t "[\s\n]"))
-        (stem (car input-separated))
-        (input (split-string
-                (mapconcat 'identity (cdr input-separated) "\n")
-                option-separator t "[\s\n]"))
-        (correct-choice-index
-         ;; Make sure correct choice is given
-         (or (cl-position-if (lambda (string) (string-match "{.*}" string)) 
input)
-             (error "Correct choice not found.  Use {} to indicate the correct 
option")))
-        (choices (mapcar (lambda (string) (replace-regexp-in-string "{\\|}" "" 
string)) input)))
-    (list (cons stem choices) (+ correct-choice-index 1))))
-
-(defun gnosis-prompt-tags--split (&optional previous-note-tags)
-  "Prompt user for tags, split string by space.
-
-Return a list of tags, split by space.  If PREVIOUS-NOTE-TAGS is
-provided, use it as the default value."
-  (let* ((previous-note-tags (or nil previous-note-tags))
-        (tags (split-string (read-from-minibuffer "Tags: " (mapconcat 
#'identity previous-note-tags " ")) " ")))
-    (setf gnosis-previous-note-tags tags)
-    (if (equal tags '("")) '("untagged") tags)))
-
-;; Collecting note ids
+(defun gnosis-tags--update (tags)
+  "Update db for TAGS."
+  (emacsql-with-transaction gnosis-db
+    (cl-loop for tag in tags
+            do (gnosis--insert-into 'tags `[,tag]))))
+
+(cl-defun gnosis-tags--prompt (&key (prompt "Tags (seperated by ,): ")
+                                   (predicate nil)
+                                   (require-match nil)
+                                   (initial-input nil))
+  "Prompt user for tags.
+
+Outputs only unique tags."
+  (gnosis-tags-refresh)
+  (let* ((tags (gnosis-tags-get-all))
+        (input (delete-dups
+                (completing-read-multiple
+                 prompt tags predicate require-match initial-input))))
+    input))
+
+(cl-defun gnosis-tags-prompt ()
+  "Tag prompt for adding notes.
+
+If you only require a tag prompt, refer to `gnosis-tags--prompt'."
+  (let ((input (gnosis-tags--prompt)))
+    (when input
+      (gnosis-tags--update input)
+      (setf gnosis-previous-note-tags input))
+    (or input '("untagged"))))
+
+(defun gnosis-tags-get-all ()
+  "Output all tags from database."
+  (gnosis-select '* 'tags '1=1 t))
+
+(defun gnosis-tags-refresh ()
+  "Refresh tags value."
+  (let ((tags (gnosis-get-tags--unique)))
+    ;; Delete all values from tags table.
+    (gnosis--delete 'tags '1=1)
+    ;; Insert all unique tags from notes.
+    (emacsql-with-transaction gnosis-db
+      (cl-loop for tag in tags
+              do (gnosis--insert-into 'tags `[,tag])))))
+
+;; Links
+(defun gnosis-extract-id-links (input &optional start)
+  "Extract all link IDs from INPUT string and return them as a list.
+
+START is the search starting position, used internally for recursion."
+  (let ((start (or start 0)))
+    (if (string-match "\\[\\[id:\\([^]]+\\)\\]\\[" input start)
+        (cons (match-string 1 input)
+              (gnosis-extract-id-links input (match-end 0)))
+      nil)))
 
 ;; TODO: Rewrite this! Tags should be an input of strings,
 ;; interactive handling should be done by "helper" funcs
@@ -1286,10 +1265,10 @@ QUERY: String value,"
         (gnosis-review-get-due-notes))
        ;; All notes for tags
        ((and tags (null due) (null deck))
-        (gnosis-select-by-tag (gnosis-tag-prompt)))
+        (gnosis-select-by-tag (gnosis-tags--prompt :require-match t)))
        ;; All due notes for tags
        ((and tags due (null deck))
-        (gnosis-select-by-tag (gnosis-tag-prompt) t))
+        (gnosis-select-by-tag (gnosis-tags--prompt :require-match t) t))
        ;; All notes for deck
        ((and (null tags) (null due) deck)
         (gnosis-get-deck-notes deck nil))
@@ -1447,45 +1426,31 @@ SUCCESS is a boolean value, t for success, nil for 
failure."
   (setf gnosis-due-notes-total (length (gnosis-review-get-due-notes))))
 
 (defun gnosis-review-mcq (id)
-  "Display multiple choice answers for question ID."
-  (gnosis-display-question id)
-  (gnosis-display-image id)
-  (when gnosis-mcq-display-choices
-    (gnosis-display-mcq-options id))
-  (let* ((choices (gnosis-get 'options 'notes `(= id ,id)))
-        (answer (nth (- (gnosis-get 'answer 'notes `(= id ,id)) 1) choices))
+  "Review MCQ thema with ID."
+  (gnosis-display-keimenon (gnosis-get 'keimenon 'notes `(= id ,id)))
+  (let* ((answer (car (gnosis-get 'apocalypse 'notes `(= id ,id))))
         (user-choice (gnosis-mcq-answer id))
         (success (string= answer user-choice)))
     (gnosis-display-correct-answer-mcq answer user-choice)
-    (gnosis-display-extra id)
+    (gnosis-display-parathema (gnosis-get 'parathema 'extras '(= id ,id)))
     (gnosis-display-next-review id success)
     success))
 
 (defun gnosis-review-basic (id)
   "Review basic type note for ID."
-  (gnosis-display-question id)
-  (gnosis-display-image id)
-  (gnosis-display-hint (gnosis-get 'options 'notes `(= id ,id)))
-  (let* ((answer (gnosis-get 'answer 'notes `(= id ,id)))
-        (user-input (read-string "Answer: "))
-        (success (gnosis-compare-strings answer user-input)))
-    (gnosis-display-basic-answer answer success user-input)
-    (gnosis-display-extra id)
-    (gnosis-display-next-review id success)
-    success))
-
-(defun gnosis-review-y-or-n (id)
-  "Review y-or-n type note for ID."
-  (gnosis-display-question id)
-  (gnosis-display-image id)
-  (gnosis-display-hint (gnosis-get 'options 'notes `(= id ,id)))
-  (let* ((answer (gnosis-get 'answer 'notes `(= id ,id)))
-        (user-input (read-char-choice "[y]es or [n]o: " '(?y ?n)))
-        (success (equal answer user-input)))
-    (gnosis-display-y-or-n-answer :answer answer :success success)
-    (gnosis-display-extra id)
-    (gnosis-display-next-review id success)
-    success))
+  (let* ((hypothesis (car (gnosis-get 'hypothesis 'notes `(= id ,id))))
+        (parathema (gnosis-get 'parathema 'extras `(= id ,id)))
+        (keimenon (gnosis-get 'keimenon 'notes `(= id ,id)))
+        (apocalypse (car (gnosis-get 'apocalypse 'notes `(= id ,id)))))
+    (gnosis-display-keimenon keimenon)
+    (gnosis-display-hint hypothesis)
+    (let* ((answer apocalypse)
+          (user-input (read-string "Answer: "))
+          (success (gnosis-compare-strings answer user-input)))
+      (gnosis-display-basic-answer answer success user-input)
+      (gnosis-display-parathema parathema)
+      (gnosis-display-next-review id success)
+      success)))
 
 (defun gnosis-review-cloze--input (cloze)
   "Prompt for user input during cloze review.
@@ -1496,10 +1461,11 @@ If user-input is equal to CLOZE, return t."
 
 (defun gnosis-review-cloze (id)
   "Review cloze type note for ID."
-  (let* ((main (gnosis-get 'main 'notes `(= id ,id)))
-        (clozes (gnosis-get 'answer 'notes `(= id ,id)))
+  (let* ((main (gnosis-get 'keimenon 'notes `(= id ,id)))
+        (clozes (gnosis-get 'apocalypse 'notes `(= id ,id)))
         (num 0) ;; Number of clozes revealed
-        (hints (gnosis-get 'options 'notes `(= id ,id)))
+        (hints (gnosis-get 'hypothesis 'notes `(= id ,id)))
+        (parathema (gnosis-get 'parathema 'extras `(= id ,id)))
         (success nil))
     ;; Quick fix for old cloze note versions.
     (cond ((and (stringp hints) (string-empty-p hints))
@@ -1526,19 +1492,18 @@ If user-input is equal to CLOZE, return t."
                    (cl-return)))
             ;; Update note after all clozes are revealed successfully
             finally (setq success t))
-    (gnosis-display-extra id)
+    (gnosis-display-parathema parathema)
     (gnosis-display-next-review id success)
     success))
 
 (defun gnosis-review-mc-cloze (id)
   "Review MC-CLOZE note of ID."
-  (let ((main (gnosis-get 'main 'notes `(= id ,id)))
+  (let ((main (gnosis-get 'keimenon 'notes `(= id ,id)))
        ;; Cloze needs to be a list, we take car as the answer
-       (cloze (list (gnosis-get 'answer 'notes `(= id ,id))))
+       (cloze (list (gnosis-get 'hypothesis 'notes `(= id ,id))))
        (user-choice nil)
        (success nil))
     (gnosis-display-cloze-string main cloze nil nil nil)
-    (gnosis-display-image id)
     (setf user-choice (gnosis-mcq-answer id)
          success (string= user-choice (car cloze)))
     (if success
@@ -1606,7 +1571,7 @@ If NEW? is non-nil, increment new notes log by 1."
     ;; Fix sync by adding a small delay, `vc-pull' is async.
     (sit-for 0.3)
     ;; Reopen gnosis-db after pull
-    (setf gnosis-db (emacsql-sqlite-open (expand-file-name "gnosis.db" dir)))))
+    (setf gnosis-db gnosis-db)))
 
 (defun gnosis-review-commit (note-num)
   "Commit review session on git repository.
@@ -1635,8 +1600,8 @@ editing NOTE with it's new contents.
 
 After done editing, call `gnosis-review-actions' with SUCCESS NOTE
 NOTE-COUNT."
-  (gnosis-edit-save-exit)
-  (gnosis-edit-note note)
+  (gnosis-edit-thema note)
+  (setf gnosis-review-editing-p t)
   (recursive-edit)
   (gnosis-review-actions success note note-count))
 
@@ -1668,6 +1633,13 @@ be called with new SUCCESS value plus NOTE & NOTE-COUNT."
   (gnosis-display-next-review note success)
   (gnosis-review-actions success note note-count))
 
+;; (defun gnosis-review-action--read (id)
+;;   "Open link for note at extras."
+;;   (let* ((extras (gnosis-get 'extra-notes 'extras `(= id ,id)))
+;;      (ids (gnosis-extract-id-links extras))
+;;      ())
+;;     )
+
 (defun gnosis-review-actions (success note note-count)
   "Specify action during review of note.
 
@@ -1723,7 +1695,7 @@ NOTE-COUNT: Total notes to be commited for session."
   ;; Select review type
   (let ((review-type
         (gnosis-completing-read "Review: "
-                                '("Due notes" 
+                                '("Due notes"
                                   "Due notes of deck"
                                   "Due notes of specified tag(s)"
                                   "Overdue notes"
@@ -1743,159 +1715,262 @@ NOTE-COUNT: Total notes to be commited for session."
                            (gnosis-collect-note-ids :deck 
(gnosis--get-deck-id))))
       ("All notes of tag(s)" (gnosis-review-session (gnosis-collect-note-ids 
:tags t))))))
 
+(defun gnosis-add-thema-fields (deck-id type keimenon hypothesis apocalypse 
parathema tags suspend links)
+  "Insert fields for new note.
+
+DECK-ID: Deck ID for new thema.
+TYPE: Note type e.g \"mcq\"
+KEIMENON: Note's keimenon
+HYPOTHESIS: Thema hypothesis, e.g choices for mcq for OR hints for
+cloze/basic thema
+APOCALYPSE: Correct answer for note, for MCQ is an integer while for
+cloze/basic a string/list of the right answer(s)
+PARATHEMA: Parathema information to display after the apocalypse
+TAGS: Tags to organize notes
+SUSPEND: Integer value of 1 or 0, where 1 suspends the card.
+LINKS: List of id links."
+  (cl-assert (integerp deck-id) nil "Deck ID must be an integer")
+  (cl-assert (stringp type) nil "Type must be a string")
+  (cl-assert (stringp keimenon) nil "Keimenon must be a string")
+  (cl-assert (listp hypothesis) nil "Hypothesis value must be a list")
+  (cl-assert (listp apocalypse) nil "Apocalypse value must be a list")
+  (cl-assert (stringp parathema) nil "Parathema must be a string")
+  (cl-assert (listp tags) nil "Tags must be a list")
+  (cl-assert (listp links) nil "Links must be a list")
+  (let* ((note-id (gnosis-generate-id)))
+    (emacsql-with-transaction gnosis-db
+      ;; Refer to `gnosis-db-schema-SCHEMA' e.g `gnosis-db-schema-review-log'
+      (gnosis--insert-into 'notes `([,note-id ,(downcase type) ,keimenon 
,hypothesis ,apocalypse ,tags ,deck-id]))
+      (gnosis--insert-into 'review  `([,note-id ,gnosis-algorithm-gnosis-value
+                                               
,gnosis-algorithm-amnesia-value]))
+      (gnosis--insert-into 'review-log `([,note-id ,(gnosis-algorithm-date)
+                                                  ,(gnosis-algorithm-date) 0 0 
0 0 ,suspend 0]))
+      (gnosis--insert-into 'extras `([,note-id ,parathema]))
+      (cl-loop for link in links
+              do (gnosis--insert-into 'links `([,note-id ,link]))))))
+
+(defun gnosis-update-thema (id keimenon hypothesis apocalypse parathema tags 
links)
+  "Update thema entry for ID."
+  (let ((id (if (stringp id) (string-to-number id) id))) ;; Make sure we 
provided the id as a number.
+    (emacsql-with-transaction gnosis-db
+      (gnosis-update 'notes `(= keimenon ,keimenon) `(= id ,id))
+      (gnosis-update 'notes `(= hypothesis ',hypothesis) `(= id ,id))
+      (gnosis-update 'notes `(= apocalypse ',apocalypse) `(= id ,id))
+      (gnosis-update 'extras `(= parathema ,parathema) `(= id ,id))
+      (gnosis-update 'notes `(= tags ',tags) `(= id ,id))
+      (cl-loop for link in links
+              do (gnosis-update 'links `(= dest ,link) `(= source ,id))))))
+
+;;;;;;;;;;;;;;;;;;;;;; THEMA HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; These functions provide assertions depending on the type of thema.
+;;
+;; Each thema should use a helper function that calls to provide
+;; assertions, such as length of hypothesis and apocalypse, for said
+;; thema.
+;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun gnosis-add-thema--basic (id deck-id type keimenon hypothesis apocalypse 
parathema tags suspend links)
+  "Default format for adding a thema.
+
+DECK-ID: Integer value of deck-id.
+TYPE: String representing the type of note.
+KEIMENON: String for the thema text.
+HYPOTHESIS: List of a signle string.
+APOCALYPSE: List of a single string.
+PARATHEMA: String for the parathema text.
+TAGS: List of thema tags.
+SUSPEND: Integer value of 0 for nil and 1 for true (suspended).
+LINKS: List of id links in PARATHEMA."
+  (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+  (cl-assert (stringp type) nil "Type must be an integer.")
+  (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+  (cl-assert (or (null hypothesis)
+                (and (listp hypothesis)
+                     (= (length hypothesis) 1)))
+            nil "Hypothesis value must be a list of a single item or nil.")
+  (cl-assert (and (listp apocalypse)
+                 (= (length apocalypse) 1))
+            nil "Apocalypse value must be a list of a signle item")
+  (cl-assert (listp tags) nil "Tags must be a list.")
+  (cl-assert (or (= suspend 0)
+                (= suspend 1))
+            nil "Suspend value must either 0 or 1")
+  (cl-assert (listp links) nil "Links must be a list")
+  (if (equal id "NEW")
+      (gnosis-add-thema-fields deck-id type keimenon (or hypothesis (list "")) 
apocalypse parathema tags suspend links)
+    (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags 
links)))
+
+(defun gnosis-add-thema--double (id deck-id type keimenon hypothesis 
apocalypse parathema tags suspend links)
+  "Double thema format.
+
+Changes TYPE to basic & inserts a second basic thema with APOCALYPSE
+and KEIMENON reversed."
+  (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+  (cl-assert (stringp type) nil "Type must be an integer.")
+  (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+  (cl-assert (listp hypothesis) nil "Hypothesis value must be a list.")
+  (cl-assert (and (listp apocalypse) (= (length apocalypse) 1))
+            nil "Apocalypse value must be a list of a signle item")
+  (cl-assert (listp tags) nil "Tags must be a list.")
+  (cl-assert (or (= suspend 0) (= suspend 1)) nil "Suspend value must either 0 
or 1")
+  (cl-assert (listp links) nil "Links must be a list")
+  ;; Change type to basic
+  (let ((type "basic")
+       (hypothesis (or hypothesis (list ""))))
+    (if (equal id "NEW")
+       (progn
+         (gnosis-add-thema-fields deck-id type keimenon hypothesis apocalypse 
parathema tags suspend links)
+         (gnosis-add-thema-fields deck-id type (car apocalypse) hypothesis 
(list keimenon) parathema tags suspend links))
+      ;; There should not be a double type thema in database to
+      ;; update.  This is used for testing purposes.
+      (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags 
links))))
+
+(defun gnosis-add-thema--mcq (id deck-id type keimenon hypothesis apocalypse 
parathema tags suspend links)
+  "Default format for adding a thema.
+
+ID: Thema ID, either an integer value or NEW.
+DECK-ID: Integer value of deck-id.
+TYPE: String representing the type of note.
+KEIMENON: String for the thema text.
+HYPOTHESIS: List of a signle string.
+APOCALYPSE: List of a single string.
+PARATHEMA: String for the parathema text.
+TAGS: List of thema tags.
+SUSPEND: Integer value of 0 for nil and 1 for true (suspended).
+LINKS: List of id links in PARATHEMA."
+  (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+  (cl-assert (stringp type) nil "Type must be an integer.")
+  (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+  (cl-assert (and (listp hypothesis)
+                 (> (length hypothesis) 1))
+            nil "Hypothesis value must be a list greater than 1 item.")
+  (cl-assert (and (listp apocalypse)
+                 (= (length apocalypse) 1)
+                 (member (car apocalypse) hypothesis))
+            nil "Apocalypse value must be a single item, member of the 
Hypothesis")
+  (cl-assert (listp tags) nil "Tags must be a list.")
+  (cl-assert (or (= suspend 0)
+                (= suspend 1))
+            nil "Suspend value must either 0 or 1")
+  (cl-assert (listp links) nil "Links must be a list")
+  (if (equal id "NEW")
+      (gnosis-add-thema-fields deck-id type keimenon (or hypothesis (list "")) 
apocalypse parathema tags suspend links)
+    (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags 
links)))
+
+(defun gnosis-add-thema--cloze (id deck-id type keimenon hypothesis apocalypse 
parathema tags suspend links)
+  "Add cloze type thema."
+  (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+  (cl-assert (stringp type) nil "Type must be an integer.")
+  (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+  (cl-assert (or (= (length apocalypse) (length hypothesis))
+                (null hypothesis))
+            nil "Hypothesis value must be a list or nil, equal in length of 
Apocalypse.")
+  (cl-assert (listp apocalypse) nil "Apocalypse value must be a list.")
+  (cl-assert (listp tags) nil "Tags must be a list.")
+  (cl-assert (or (= suspend 0)
+                (= suspend 1))
+            nil "Suspend value must either 0 or 1")
+  (cl-assert (listp links) nil "Links must be a list")
+  (cl-assert (gnosis-cloze-check keimenon apocalypse) nil "Clozes (apocalypse) 
values are not part of keimenon")
+  (if (equal id "NEW")
+      (gnosis-add-thema-fields deck-id type keimenon (or hypothesis (list "")) 
apocalypse parathema tags suspend links)
+    (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags 
links)))
+
+(defun gnosis-save-thema (thema deck)
+  "Save THEMA for DECK."
+  (let* ((id (nth 0 thema))
+        (type (nth 1 thema))
+        (keimenon (nth 2 thema))
+        (hypothesis (and (nth 3 thema) (mapcar (lambda (item) 
(string-remove-prefix "- " item))
+                                               (split-string (nth 3 thema) 
gnosis-org-separator))))
+        (apocalypse (and (nth 4 thema) (mapcar (lambda (item)
+                                                 "Replace 
`gnosis-org-separator'."
+                                                 (string-remove-prefix "- " 
item))
+                                               (split-string (nth 4 thema) 
gnosis-org-separator))))
+        (parathema (or (nth 5 thema) ""))
+        (tags (nth 6 thema))
+        (links (gnosis-extract-id-links parathema))
+        (thema-func (cdr (assoc (downcase type)
+                                 (mapcar (lambda (pair) (cons (downcase (car 
pair))
+                                                         (cdr pair)))
+                                         gnosis-thema-types)))))
+    ;; (message "asdfs")
+    (funcall thema-func id deck type keimenon hypothesis apocalypse parathema 
tags 0 links)))
 
-;; Editing notes
-(defun gnosis-edit-read-only-values (&rest values)
-  "Make the provided VALUES read-only in the whole buffer."
-  (goto-char (point-min))
-  (dolist (value values)
-    (while (search-forward value nil t)
-      (put-text-property (match-beginning 0) (match-end 0) 'read-only t)))
-  (goto-char (point-min)))
-
-(cl-defun gnosis-edit-note (id)
-  "Edit the contents of a note with the given ID.
-
-This function creates an Emacs Lisp buffer named *gnosis-edit* on the
-same window and populates it with the values of the note identified by
-the specified ID using `gnosis-export-note'.  The note values are
-inserted as keywords for the `gnosis-edit-update-note' function.
-
-To make changes, edit the values in the buffer, and then evaluate the
-`gnosis-edit-update-note' expression to save the changes.
-
-RECURSIVE-EDIT: If t, exit `recursive-edit' after finishing editing.
-It should only be t when starting a recursive edit, when editing a
-note during a review session.
-
-The buffer automatically indents the expressions for readability.
-After finishing editing, evaluate the entire expression to apply the
-changes."
-  (pop-to-buffer-same-window (get-buffer-create "*gnosis-edit*"))
-  (gnosis-edit-mode)
-  (erase-buffer)
-  (insert ";;\n;; You are editing a gnosis note.\n\n")
-  (insert "(gnosis-edit-update-note ")
-  (gnosis-export-note id)
-  (insert ")")
-  (insert "\n\n;; After finishing editing, save changes with `<C-c> <C-c>'\n;; 
Avoid exiting without saving.")
-  (indent-region (point-min) (point-max))
-  ;; Insert id & fields as read-only values
-  (gnosis-edit-read-only-values (format ":id %s" id) ":main" ":options" 
":answer"
-                               ":tags" ":extra-notes" ":image" ":second-image"
-                               ":gnosis" ":amensia" ":suspend")
-  (local-set-key (kbd "C-c C-c") (lambda () (interactive) 
(gnosis-edit-note-save-exit))))
-
-(defun gnosis-assert-int-or-nil (value description)
-  "Assert that VALUE is an integer or nil.
-
-DESCRIPTION is a string that describes the value."
-  (unless (or (null value) (integerp value))
-    (error "Invalid value: %s, %s" value description)))
-
-(defun gnosis-assert-float-or-nil (value description &optional less-than-1)
-  "Assert that VALUE is a float or nil.
-
-DESCRIPTION is a string that describes the value.
-LESS-THAN-1: If t, assert that VALUE is a float less than 1."
-  (if less-than-1
-      (unless (or (null value) (and (floatp value) (< value 1)))
-       (error "Invalid value: %s, %s" value description))
-    (unless (or (null value) (floatp value))
-      (error "Invalid value: %s, %s" value description))))
-
-(defun gnosis-assert-number-or-nil (value description)
-  "Assert that VALUE is a number or nil.
-
-DESCRIPTION is a string that describes the value."
-  (unless (or (null value) (numberp value))
-    (error "Invalid value: %s, %s" value description)))
-
-(cl-defun gnosis-edit-save-exit ()
-  "Save edits and exit using EXIT-FUNC, with ARGS."
+;;;###autoload
+(defun gnosis-add-thema (deck type &optional keimenon hypothesis apocalypse 
parathema tags example)
+  "Add thema with TYPE in DECK."
+  (interactive (list
+               (gnosis--get-deck-name)
+               (downcase (completing-read "Select type: " 
gnosis-thema-types))))
+  (pop-to-buffer "*Gnosis NEW*")
+  (with-current-buffer "*Gnosis NEW*"
+    (let ((inhibit-read-only 1))
+      (erase-buffer))
+    (insert "#+DECK: " deck)
+    (gnosis-edit-mode)
+    (gnosis-org--insert-thema "NEW" type keimenon hypothesis apocalypse 
parathema tags example))
+  (search-backward "keimenon")
+  (forward-line))
+
+(defun gnosis-export-note (id)
+  "Export note with ID."
+  (let ((note-data (append (gnosis-select '[type keimenon hypothesis 
apocalypse tags] 'notes `(= id ,id) t)
+                          (gnosis-select 'parathema 'extras `(= id ,id) t))))
+      (gnosis-org--insert-thema (number-to-string id)
+                               (nth 0 note-data)
+                               (nth 1 note-data)
+                               (concat (string-remove-prefix "\n" 
gnosis-org-separator)
+                                       (mapconcat 'identity (nth 2 note-data) 
gnosis-org-separator))
+                               (concat (string-remove-prefix "\n" 
gnosis-org-separator)
+                                       (mapconcat 'identity (nth 3 note-data) 
gnosis-org-separator))
+                               (nth 5 note-data)
+                               (nth 4 note-data))))
+
+(defun gnosis-edit-thema (id)
+  "Edit note with ID."
+  (with-current-buffer (pop-to-buffer "*Gnosis Edit*")
+    (let ((inhibit-read-only 1)
+         (deck-name (gnosis--get-deck-name
+                     (gnosis-get 'deck-id 'notes `(= id ,id)))))
+      (erase-buffer)
+      (insert "#+DECK: " deck-name))
+    (gnosis-edit-mode)
+    (gnosis-export-note id)
+    (search-backward "keimenon")
+    (forward-line)))
+
+(defun gnosis-save ()
+  "Save themas in current buffer."
   (interactive)
-  (when (get-buffer "*gnosis-edit*")
-    (switch-to-buffer "*gnosis-edit*")
-    (eval-buffer)
-    (quit-window t)
-    (gnosis-dashboard-return)))
-
-(cl-defun gnosis-edit-note-save-exit ()
-  "Save edits and exit using EXIT-FUNC, with ARGS."
+  (let ((themas (gnosis-org-parse-themas))
+       (deck (gnosis--get-deck-id (gnosis-org-parse--deck-name))))
+    (cl-loop for thema in themas
+            do (gnosis-save-thema thema deck))
+    (gnosis-edit-quit)))
+
+(defun gnosis-edit-quit ()
+  "Quit recrusive edit & kill current buffer."
   (interactive)
-  (when (get-buffer "*gnosis-edit*")
-    (switch-to-buffer "*gnosis-edit*")
-    (eval-buffer)
-    (quit-window t)
+  (kill-buffer-and-window)
+  (when gnosis-review-editing-p
+    (setf gnosis-review-editing-p nil)
     (exit-recursive-edit)))
 
 (defvar-keymap gnosis-edit-mode-map
-  :doc "gnosis-edit keymap"
-  "C-c C-c" #'gnosis-edit-save-exit)
+  :doc "gnosis org mode map"
+  "C-c C-c" #'gnosis-save
+  "C-c C-k" #'gnosis-edit-quit)
 
-(define-derived-mode gnosis-edit-mode emacs-lisp-mode "Gnosis EDIT"
-  "Gnosis Edit Mode."
+(define-derived-mode gnosis-edit-mode org-mode "Gnosis Org"
+  "Gnosis Org Mode."
   :interactive nil
   :lighter " Gnosis Edit"
-  :keymap gnosis-edit-mode-map)
-
-(cl-defun gnosis-edit-update-note (&key id main options answer tags 
(extra-notes nil) (image nil)
-                                       (second-image nil) gnosis amnesia 
suspend)
-  "Update note with id value of ID.
-
-ID: Note id
-MAIN: Main part of note, the stem part of MCQ, question for basic, etc.
-OPTIONS: Options for mcq type notes/Hint for basic & cloze type notes
-ANSWER: Answer for MAIN
-TAGS: Tags for note, used to organize & differentiate between notes
-EXTRA-NOTES: Notes to display after user-input
-IMAGE: Image to display before user-input
-SECOND-IMAGE: Image to display after user-input
-GNOSIS: Gnosis score
-AMNESIA: Amnesia value
-SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend"
-  (cl-assert (stringp main) nil "Main must be a string")
-  (cl-assert (or (stringp image) (null image)) nil
-            "Image must be a string, path to image file from 
`gnosis-images-dir', or nil")
-  (cl-assert (or (stringp second-image) (null second-image)) nil
-            "Second-image must be a string, path to image file from 
`gnosis-images-dir', or nil")
-  (cl-assert (or (stringp extra-notes) (null extra-notes)) nil
-            "Extra-notes must be a string, or nil")
-  (cl-assert (and (listp tags) (cl-every #'stringp tags)) nil "Tags must be a 
list of strings")
-  (cl-assert (and (listp gnosis) (length= gnosis 3) (cl-every #'floatp gnosis))
-            nil "gnosis must be a list of 3 floats")
-  (cl-assert (or (stringp options) (and (listp options) (cl-every #'(lambda 
(x) (or (stringp x) (null x)))
-                                                                 options)))
-            nil "Options must be a string or a list of strings")
-  (cl-assert (and (numberp suspend) (or (= suspend 0) (= suspend 1))) nil 
"Suspend must be either 0 or 1")
-  (when (and (string= (gnosis-get-type id) "cloze")
-            (not (stringp options)))
-    (cl-assert (or (listp options) (stringp options)) nil "Options must be a 
list or a string.")
-    (cl-assert (gnosis-cloze-check main answer) nil "Clozes are not part of 
the question (main).")
-    (cl-assert (>= (length answer) (length options)) nil
-              "Hints (options) must be equal or less than clozes (answer).")
-    (cl-assert (cl-every (lambda (item) (or (null item) (stringp item))) 
options) nil "Hints (options) must be either nil or a string."))
-  ;; Construct the update clause for the emacsql update statement.
-  (cl-loop for (field . value) in `((main . ,main)
-                                   (options . ,options)
-                                   (answer . ,answer)
-                                   (tags . ,tags)
-                                   (extra-notes . ,extra-notes)
-                                   (images . ,image)
-                                   (extra-image . ,second-image)
-                                   (gnosis . ',gnosis)
-                                   (amnesia . ,amnesia)
-                                   (suspend . ,suspend))
-           when value
-           do (cond ((memq field '(extra-notes images extra-image))
-                    (gnosis-update 'extras `(= ,field ,value) `(= id ,id)))
-                   ((memq field '(gnosis amnesia))
-                    (gnosis-update 'review `(= ,field ,value) `(= id ,id)))
-                   ((eq field 'suspend)
-                    (gnosis-update 'review-log `(= ,field ,value) `(= id ,id)))
-                   ((listp value)
-                    (gnosis-update 'notes `(= ,field ',value) `(= id ,id)))
-                   (t (gnosis-update 'notes `(= ,field ,value) `(= id ,id))))))
+  :keymap gnosis-edit-mode-map
+  (setq header-line-format (format " Save thema by running %s or %s to quit"
+                                  (propertize "C-c C-c" 'face 
'help-key-binding)
+                                  (propertize "C-c C-k" 'face 
'help-key-binding))))
 
 (defun gnosis-validate-custom-values (new-value)
   "Validate the structure and values of NEW-VALUE for gnosis-custom-values."
@@ -2434,7 +2509,7 @@ If STRING-SECTION is nil, apply FACE to the entire 
STRING."
                                          :tags note-tags)
               (gnosis-add-note--mcq :deck deck-name
                                     :question "Which one is the capital of 
Greece?"
-                                    :choices '("Athens" "Sparta" "Nafplio" 
"Constantinople")
+                                    :choices '("Athens" "Sparta" "Rome" 
"Constantinople")
                                     :correct-answer 1
                                     :extra "Athens (Ἀθήνα) is the largest city 
of Greece & one of the world's oldest cities, with it's recorded history 
spanning over 3,500 years."
                                     :tags note-tags)
@@ -2513,7 +2588,7 @@ DATE: Integer, used with `gnosis-algorithm-date' to get 
previous dates."
   "Output the average daily notes reviewed for current year.
 
 Skips days where no note was reviewed."
-  (let ((reviews (gnosis-select 'reviewed-total 'activity-log '1=1 t)))
+  (let ((reviews (gnosis-select 'reviewed-total 'activity-log '(> 
reviewed-total 0) t)))
     (if (null reviews) 0
       (format "%.2f" (/ (apply '+ reviews) (float (length reviews)))))))
 
@@ -2521,7 +2596,7 @@ Skips days where no note was reviewed."
   "Edit note with ID."
   (interactive)
   (let ((id (tabulated-list-get-id)))
-    (gnosis-edit-note id)))
+    (gnosis-edit-thema id)))
 
 (defun gnosis-dashboard-suspend-note ()
   "Suspend note."
@@ -2551,8 +2626,8 @@ Skips days where no note was reviewed."
   :doc "Keymap for notes dashboard."
   "e" #'gnosis-dashboard-edit-note
   "s" #'gnosis-dashboard-suspend-note
-  "C-s" #'gnosis-dashboard-search-note
-  "a" #'gnosis-add-note
+  "SPC" #'gnosis-dashboard-search-note
+  "a" #'gnosis-add-thema
   "r" #'gnosis-dashboard-return
   "g" #'gnosis-dashboard-return
   "d" #'gnosis-dashboard-delete
@@ -2568,7 +2643,7 @@ Skips days where no note was reviewed."
   (cl-assert (listp note-ids))
   (let ((entries (emacsql gnosis-db
                          `[:select
-                           [notes:id notes:main notes:options notes:answer
+                           [notes:id notes:keimenon notes:hypothesis 
notes:apocalypse
                                      notes:tags notes:type review-log:suspend]
                            :from notes
                            :join review-log :on (= notes:id review-log:id)
@@ -2589,9 +2664,9 @@ Skips days where no note was reviewed."
   (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
   (gnosis-dashboard-enable-mode)
   (gnosis-dashboard-notes-mode)
-  (setf tabulated-list-format `[("Main" ,(/ (window-width) 4) t)
-                                ("Options" ,(/ (window-width) 6) t)
-                                ("Answer" ,(/ (window-width) 6) t)
+  (setf tabulated-list-format `[("Keimenon" ,(/ (window-width) 4) t)
+                                ("Hypothesis" ,(/ (window-width) 6) t)
+                                ("Apocalypse" ,(/ (window-width) 6) t)
                                 ("Tags" ,(/ (window-width) 5) t)
                                 ("Type" ,(/ (window-width) 10) t)
                                 ("Suspend" ,(/ (window-width) 6) t)]
@@ -2630,12 +2705,31 @@ Skips days where no note was reviewed."
 (defun gnosis-dashboard-rename-tag (&optional tag new-tag )
   "Rename TAG to NEW-TAG."
   (interactive)
-  (let ((new-tag (or new-tag (read-string "News tag name: ")))
+  (let ((new-tag (or new-tag (read-string "New tag name: ")))
        (tag (or tag (tabulated-list-get-id))))
     (cl-loop for note in (gnosis-get-tag-notes tag)
             do (let* ((tags (car (gnosis-select '[tags] 'notes `(= id ,note) 
t)))
                       (new-tags (cl-substitute new-tag tag tags :test 
#'string-equal)))
-                 (gnosis-update 'notes `(= tags ',new-tags) `(= id ,note))))))
+                 (gnosis-update 'notes `(= tags ',new-tags) `(= id ,note))))
+    ;; Update tags in database
+    (gnosis-tags--update (gnosis-tags-get-all))
+    ;; Output tags anew
+    (gnosis-dashboard-output-tags)))
+
+(defun gnosis-dashboard-delete-tag (&optional tag)
+  "Rename TAG to NEW-TAG."
+  (interactive)
+  (let ((tag (or tag (tabulated-list-get-id))))
+    (when (y-or-n-p (format "Delete tag %s?" (propertize tag 'face 
'font-lock-keyword-face)))
+      (cl-loop for note in (gnosis-get-tag-notes tag)
+              do (let* ((tags (car (gnosis-select '[tags] 'notes `(= id ,note) 
t)))
+                        (new-tags (remove tag tags)))
+                   (gnosis-update 'notes `(= tags ',new-tags) `(= id ,note))))
+      ;; Update tags in database
+      (gnosis-tags--update (gnosis-tags-get-all))
+      ;; Output tags anew
+      (gnosis-dashboard-output-tags))))
+
 
 (defun gnosis-dashboard-rename-deck (&optional deck-id new-name)
   "Rename deck where DECK-ID with NEW-NAME."
@@ -2665,6 +2759,7 @@ Skips days where no note was reviewed."
   "e" #'gnosis-dashboard-rename-tag
   "s" #'gnosis-dashboard-suspend-tag
   "r" #'gnosis-dashboard-rename-tag
+  "d" #'gnosis-dashboard-delete-tag
   "g" #'gnosis-dashboard-return)
 
 (define-minor-mode gnosis-dashboard-tags-mode
@@ -2673,7 +2768,8 @@ Skips days where no note was reviewed."
 
 (defun gnosis-dashboard-output-tags (&optional tags)
   "Format gnosis dashboard with output of TAGS."
-  (let ((tags (or tags (gnosis-get-tags--unique))))
+  (gnosis-tags-refresh) ;; Refresh tags
+  (let ((tags (or tags (gnosis-tags-get-all))))
     (pop-to-buffer-same-window gnosis-dashboard-buffer-name)
     (gnosis-dashboard-enable-mode)
     (gnosis-dashboard-tags-mode)
@@ -2760,7 +2856,7 @@ When called with called with a prefix, unsuspend all 
notes of deck."
   "q" #'quit-window
   "h" #'gnosis-dashboard-menu
   "r" #'gnosis-review
-  "a" #'gnosis-add-note
+  "a" #'gnosis-add-thema
   "A" #'gnosis-add-deck
   "s" #'gnosis-dashboard-suffix-query
   "n" #'(lambda () (interactive) (gnosis-dashboard-output-notes 
(gnosis-collect-note-ids)))
@@ -2826,7 +2922,8 @@ DASHBOARD-TYPE: either Notes or Decks to display the 
respective dashboard."
                  (setf gnosis-dashboard--selected-ids
                        (append gnosis-dashboard--selected-ids (list id)))
                   (overlay-put ov 'face 'highlight)
-                  (overlay-put ov 'gnosis-mark t))))
+                  (overlay-put ov 'gnosis-mark t)))
+             (forward-line))
           (message "No entry at point"))
       (message "Not in a tabulated-list-mode"))))
 
@@ -2882,8 +2979,7 @@ DASHBOARD-TYPE: either Notes or Decks to display the 
respective dashboard."
   "Launch gnosis dashboard."
   (interactive)
   ;; Refresh gnosis-db
-  (unless gnosis-testing
-    (setf gnosis-db (emacsql-sqlite-open (expand-file-name "gnosis.db" 
gnosis-dir))))
+  (setf gnosis-db gnosis-db)
   (let* ((buffer-name gnosis-dashboard-buffer-name)
         (due-log (gnosis-review-get--due-notes))
         (due-note-ids (mapcar #'car due-log)))


Reply via email to