branch: elpa/gnosis
commit 731f0ba4910c872efedf7e460d904f3d9c3be9a7
Merge: bc626d511c d034335bde
Author: Thanos Apollo <pub...@thanosapollo.org>
Commit: Thanos Apollo <pub...@thanosapollo.org>

    Release version 0.4.2.
    
    * Comment out gnosis-org sections that are under development.
       * gnosis-org should be ready by next version.
    * Fix display issues on non-grapical interface.
    * Add variable watchers for custom algorithm values.
    * Update assertions for editing notes
    
    This is a minor release with a few bug fixes.
---
 .elpaignore    |   1 +
 Makefile       |   8 +++-
 README         |   9 -----
 README.md      |   9 +++++
 doc/gnosis.org |  60 ++++++++++++------------------
 gnosis-org.el  |  92 ++++++++++++++++++++++++++++++++++++++++++++++
 gnosis.el      | 114 +++++++++++++++++++++++++++++++++++++++++++--------------
 manifest.scm   |   3 ++
 8 files changed, 222 insertions(+), 74 deletions(-)

diff --git a/.elpaignore b/.elpaignore
index d8ed22f052..9a93dadc28 100644
--- a/.elpaignore
+++ b/.elpaignore
@@ -1,3 +1,4 @@
 CONTRIBUTING.org
 LICENSE
 Makefile
+manifest.scm
diff --git a/Makefile b/Makefile
index 4ee28ad675..9b1a262a14 100644
--- a/Makefile
+++ b/Makefile
@@ -6,15 +6,21 @@ EMACS = emacs
 ORG := doc/gnosis.org
 TEXI := doc/gnosis.texi
 INFO := doc/gnosis.info
-
+TEST_FILE := gnosis-test.el
 
 all: doc
 
 doc:   $(ORG)
        $(EMACS) --batch \
+       -Q \
        --load org \
        --eval "(with-current-buffer (find-file \"$(ORG)\") 
(org-texinfo-export-to-texinfo) (org-texinfo-export-to-info) (save-buffer))" \
        --kill
 
+test:
+       $(EMACS) --batch \
+       --load $(TEST_FILE) \
+       --eval "(ert-run-tests-batch-and-exit)"
+
 clean:
        rm -f $(TEXI) $(INFO)
diff --git a/README b/README
deleted file mode 100644
index 34e8089467..0000000000
--- a/README
+++ /dev/null
@@ -1,9 +0,0 @@
-
-Gnosis (γνῶσις)
-====
-
-Gnosis (γνῶσις), pronounced "noh-sis", meaning knowledge in Greek,
-is a Spaced Repetition System for note taking and self testing.
-
-- Project's Page:             <https://thanosapollo.org/projects/gnosis/>
-- User Manual:                <https://thanosapollo.org/user-manual/gnosis/>
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..8ed18df1ef
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+# Γνῶσις | Gnosis
+
+## About
+
+Γνῶσις (gnosis), pronounced "GNU-sis", meaning knowledge in Greek,
+is a GNU Emacs Spaced Repetition System for storing knowledge.
+
+- [Project's Page](https://thanosapollo.org/projects/gnosis/)
+- [User Manual](https://elpa.nongnu.org/nongnu/doc/gnosis.html)
diff --git a/doc/gnosis.org b/doc/gnosis.org
index 15afc52cc7..9d0efb94cc 100644
--- a/doc/gnosis.org
+++ b/doc/gnosis.org
@@ -4,8 +4,8 @@
 #+language: en
 #+options: ':t toc:nil author:t email:t num:t
 #+startup: content
-#+macro: stable-version 0.4.0
-#+macro: release-date 2024-08-7
+#+macro: stable-version 0.4.2
+#+macro: release-date 2024-09-5
 #+macro: file @@texinfo:@file{@@$1@@texinfo:}@@
 #+macro: space @@texinfo:@: @@
 #+macro: kbd @@texinfo:@kbd{@@$1@@texinfo:}@@
@@ -22,15 +22,16 @@
 #+texinfo_header: @set MAINTAINERCONTACT 
@uref{mailto:pub...@thanosapollo.org,contact the maintainer}
 
 
-Gnosis is a customizable spaced repetition system designed to enhance
+Gnosis (GNU-sis) is a customizable spaced repetition system designed to enhance
 memory retention through active recall.  It allows users to set
 specific review intervals for note decks & tags, creating an optimal
-learning environment tailored to each specific topic.
+learning environment tailored to each specific topic/subject.
 
 #+texinfo: @noindent
 This manual is written for Gnosis version {{{stable-version}}}, released on 
{{{release-date}}}.
 
-+ Official manual: <https://thanosapollo.org/user-manual/gnosis>
++ Official manual:
+  + <https://elpa.nongnu.org/nongnu/doc/gnosis.html>
 + Git repositories:
   + <https://git.thanosapollo.org/gnosis>
 
@@ -260,7 +261,6 @@ name suggests, they rely on =vc= to work properly.
 Depending on your setup, =vc= might require an external package for
 the ssh passphrase dialog, such as ~x11-ssh-askpass~.
 
-
 To automatically push changes after a review session, add this to your 
configuration:
 #+begin_src emacs-lisp
 (setf gnosis-vc-auto-push t)
@@ -268,44 +268,30 @@ To automatically push changes after a review session, add 
this to your configura
 #+end_src
 
 * Configuring Note Types
-** Adjust Current Types Entries
+** Custom Note Types
 Each gnosis note type has an /interactive/ function, named
-=gnosis-add-note-TYPE=.  You can set default values for each entry by
-hard coding specific values to their keywords.
+=gnosis-add-note-TYPE= and a "hidden" function
+named =gnosis-add-note--TYPE=.  You can create your own custom interactive
+functions to ignore or hard-code specific values by using already
+defined hidden functions that handle all the logic.
 
 For example:
 
 #+begin_src emacs-lisp
-(defun gnosis-add-note-basic (deck)
-  (gnosis-add-note--basic :deck deck
-                         :question (gnosis-read-string-from-buffer "Question: 
" "")
-                         :answer (read-string "Answer: ")
-                         :hint (gnosis-hint-prompt gnosis-previous-note-hint)
-                         :extra ""
-                         :images nil
-                         :tags (gnosis-prompt-tags--split 
gnosis-previous-note-tags)))
+  (defun gnosis-add-note-custombasic (deck)
+    (gnosis-add-note--basic :deck deck
+                         :question (gnosis-read-string-from-buffer "Question: 
" "")
+                         :answer (read-string "Answer: ")
+                         :hint (gnosis-hint-prompt gnosis-previous-note-hint)
+                         :extra ""
+                         :images nil
+                         :tags (gnosis-prompt-tags--split 
gnosis-previous-note-tags)))
+  ;; Add custom note type to gnosis-note-types
+  (add-to-list 'gnosis-note-types "custombasic")
 #+end_src
 
-By evaluating the above code snippet, you won't be prompted to enter
-anything for ~extra~ & ~images~. 
-** Creating Custom Note Types
-
-Creating custom note types for gnosis is a fairly simple thing to do
-
-+ First add your NEW-TYPE to =gnosis-note-types=
-
-    #+begin_src emacs-lisp
-    (add-to-list 'gnosis-note-types "NEW-TYPE")
-  #+end_src
-+ Create an interactive function
-
-Each note type has a =gnosis-add-note-TYPE= that is used interactively
-& a "hidden function" =gnosis-add-note--TYPE= that handles all the
-logic.  You can use one of the =current gnosis-add-note--TYPE=
-functions or create one of your own.
-
-Refer to =gnosis-add-note-basic= & =gnosis-add-note--basic= for a simple
-example of how this is done, as well as =gnosis-add-note-double=.
+Now ~custombasic~ is available as a note type, for which you won't be prompted 
to enter
+anything for ~extra~ & ~images~.
 
 ** Development
 To make development and customization easier, gnosis comes with
diff --git a/gnosis-org.el b/gnosis-org.el
new file mode 100644
index 0000000000..e977083e58
--- /dev/null
+++ b/gnosis-org.el
@@ -0,0 +1,92 @@
+;;; gnosis-org.el --- Org module for Gnosis  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2023-2024  Thanos Apollo
+
+;; Author: Thanos Apollo <pub...@thanosapollo.org>
+;; Keywords: extensions
+;; URL: https://git.thanosapollo.org/gnosis
+;; Version: 0.0.1
+
+;; Package-Requires: ((emacs "27.2") (compat "29.1.4.2"))
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Under development.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'org)
+(require 'org-element)
+
+(defun gnosis-org--global-props (name &optional buffer)
+  "Get the plists of global org properties by NAME in BUFFER.
+
+NAME is a string representing the property name to search for.
+BUFFER defaults to the current buffer if not specified."
+  (cl-assert (stringp name) nil "NAME must be a string.")
+  (with-current-buffer (or buffer (current-buffer))
+    (let ((elements (org-element-map (org-element-parse-buffer) 'keyword
+                      (lambda (el)
+                        (when (string= (org-element-property :key el) name)
+                          el))
+                      nil t)))
+      (if elements elements
+        (message "No properties found for %s" name)
+        nil))))
+
+(defun gnosis-org--heading-props (property &optional buffer)
+  "Get the values of a custom PROPERTY from all headings in BUFFER.
+
+PROPERTY is a string representing the property name to search for.
+BUFFER defaults to the current buffer if not specified."
+  (cl-assert (stringp property) nil "PROPERTY must be a string.")
+  (with-current-buffer (or buffer (current-buffer))
+    (let ((results nil))
+      (org-element-map (org-element-parse-buffer) 'headline
+        (lambda (headline)
+          (let ((prop (org-element-property (intern (concat ":" property)) 
headline)))
+            (when prop
+              (push prop results)))))
+      (if results (reverse results)
+        (message "No custom properties found for %s" property)
+        nil))))
+;; TODO: Add support for tags.
+(cl-defun gnosis-org-insert-heading (&key main id answer type)
+  "Insert an Org heading in current buffer.
+
+- MAIN as the title.
+- ID as GNOSIS_ID.
+- ANSWER as the subheading.
+- TYPE as the note type.
+
+If BUFFER is not specified, defaults to the current buffer."
+  (cl-assert (stringp main) nil "MAIN must be a string representing the 
heading title.")
+  (cl-assert (stringp id) nil "ID must be a string representing the 
GNOSIS_ID.")
+  (cl-assert (stringp type) nil "TYPE must be a string representing the TYPE 
property.")
+  (let ((main (if (string-match-p "\n" main) (replace-regexp-in-string "\n" 
"\\\\n" main) main))
+       (answer (cond ((stringp answer)
+                      answer)
+                     ((numberp answer)
+                      (number-to-string answer))
+                     (t (mapconcat 'identity answer ", ")))))
+    (goto-char (point-max)) ;; Ensure we're at the end of the buffer
+    (insert (format "* %s\n:PROPERTIES:\n:GNOSIS_ID: %s\n:TYPE: %s\n:END:\n** 
%s\n"
+                   main id type answer))
+    (message "Inserted heading: %s with GNOSIS_ID %s and TYPE %s" main id 
type)))
+
+(provide 'gnosis-org)
+;;; gnosis-org.el ends here.
diff --git a/gnosis.el b/gnosis.el
index 3c04826e30..4619fa94fd 100644
--- a/gnosis.el
+++ b/gnosis.el
@@ -5,9 +5,9 @@
 ;; Author: Thanos Apollo <pub...@thanosapollo.org>
 ;; Keywords: extensions
 ;; URL: https://thanosapollo.org/projects/gnosis
-;; Version: 0.4.1
+;; Version: 0.4.2
 
-;; Package-Requires: ((emacs "27.2") (emacsql "4.0.0") (compat "29.1.4.2") 
(transient "0.7.2"))
+;; Package-Requires: ((emacs "27.2") (emacsql "4.0.1") (compat "29.1.4.2") 
(transient "0.7.2"))
 
 ;; 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
@@ -50,6 +50,8 @@
 (require 'gnosis-string-edit)
 (require 'gnosis-dashboard)
 
+;; (require 'gnosis-org)
+
 (require 'animate)
 
 (defgroup gnosis nil
@@ -217,7 +219,7 @@ When nil, review new notes last."
 (defvar gnosis-review-notes nil
   "Review notes.")
 
-;; TODO: Make this as a defcustom
+;; TODO: Make this as a defcustom.
 (defvar gnosis-custom-values
   '((:deck "demo" (:proto (0 1 3) :anagnosis 3 :epignosis 0.5 :agnoia 0.3 
:amnesia 0.5 :lethe 3))
     (:tag "demo" (:proto (1 2) :anagnosis 3 :epignosis 0.5 :agnoia 0.3 
:amnesia 0.45 :lethe 3)))
@@ -482,19 +484,21 @@ or =extra-image'.  Instead of using =extra-image' post 
review, prefer
 =gnosis-display-extra' which displays the =extra-image' as well.
 
 Refer to =gnosis-db-schema-extras' for informations on images stored."
-  (let* ((img (gnosis-get image 'extras `(= id ,id)))
-         (path-to-image (expand-file-name (or img "") (file-name-as-directory 
gnosis-images-dir)))
-         (image (create-image path-to-image 'png nil :width gnosis-image-width 
:height gnosis-image-height))
-         (image-width (car (image-size image t)))
-         (frame-width (window-text-width))) ;; Width of the current window in 
columns
-    (cond ((or (not img) (string-empty-p img))
-           (insert "\n\n"))
-          ((and img (file-exists-p path-to-image))
-           (let* ((padding-cols (/ (- frame-width (floor (/ image-width 
(frame-char-width)))) 2))
-                  (padding (make-string (max 0 padding-cols) ?\s)))
-             (insert "\n\n" padding)  ;; Insert padding before the image
-             (insert-image image)
-             (insert "\n\n"))))))
+  ;; Only display images on graphical env
+  (when (display-graphic-p)
+    (let* ((img (gnosis-get image 'extras `(= id ,id)))
+           (path-to-image (expand-file-name (or img "") 
(file-name-as-directory gnosis-images-dir)))
+           (image (create-image path-to-image 'png nil :width 
gnosis-image-width :height gnosis-image-height))
+           (image-width (car (image-size image t)))
+           (frame-width (window-text-width))) ;; Width of the current window 
in columns
+      (cond ((or (not img) (string-empty-p img))
+             (insert "\n\n"))
+            ((and img (file-exists-p path-to-image))
+             (let* ((padding-cols (/ (- frame-width (floor (/ image-width 
(frame-char-width)))) 2))
+                    (padding (make-string (max 0 padding-cols) ?\s)))
+               (insert "\n\n" padding)  ;; Insert padding before the image
+               (insert-image image)
+               (insert "\n\n")))))))
 
 (defun gnosis-display-mcq-options (id)
   "Display answer options for mcq note ID."
@@ -1269,7 +1273,7 @@ Optionally, add cusotm PROMPT."
   (cl-loop for tags in (gnosis-select 'tags 'notes '1=1 t)
            nconc tags into all-tags
            finally return (delete-dups all-tags)))
-;; TODO: Rewrite this using `gnosis-get-tag-notes'.
+;; TODO: Rewrite this using gnosis-get-tag-notes.
 (defun gnosis-select-by-tag (input-tags &optional due suspended-p)
   "Return note ID's for every note with INPUT-TAGS.
 
@@ -1389,8 +1393,8 @@ provided, use it as the default value."
 
 ;; Collecting note ids
 
-;; TODO: Rewrite.  Tags should be an input of strings, interactive
-;; handling should be done by "helper" funcs
+;; TODO: Rewrite this! Tags should be an input of strings,
+;; interactive handling should be done by "helper" funcs
 (cl-defun gnosis-collect-note-ids (&key (tags nil) (due nil) (deck nil) (query 
nil))
   "Return list of note ids based on TAGS, DUE, DECKS, QUERY.
 
@@ -1739,7 +1743,7 @@ NOTE-COUNT: The number of notes reviewed in the session 
to be commited."
       (error "Git not found, please install git"))
     (unless (file-exists-p (expand-file-name ".git" gnosis-dir))
       (vc-create-repo 'Git))
-    ;; TODO: Redo this using vc
+    ;; TODO: Redo this using vc.
     (unless gnosis-testing
       (shell-command (format "%s %s %s" git "add" (shell-quote-argument 
"gnosis.db")))
       (shell-command (format "%s %s %s" git "commit -m"
@@ -1834,7 +1838,7 @@ NOTE-COUNT: Total notes to be commited for session."
                      (cl-incf note-count)
                      (gnosis-review-actions success note note-count))
                 finally
-                ;; TODO: Add optional arg to repeat for specific deck/tag
+                ;; TODO: Add optional arg, repeat for specific deck/tag.
                 ;; Repeat until there are no due notes
                 (and due (gnosis-review-session (gnosis-collect-note-ids :due 
t) t note-count))))
       (gnosis-dashboard)
@@ -1982,10 +1986,12 @@ SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend"
             "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 (listp tags) nil "Tags must be a list of strings")
-  (cl-assert (and (listp gnosis) (length= gnosis 3)) nil "gnosis must be a 
list of 3 floats")
-  (cl-assert (or (stringp options) (listp options)) nil "Options must be a 
string, or a list for MCQ")
-  (cl-assert (or (= suspend 0) (= suspend 1)) nil "Suspend must be either 0 or 
1")
+  (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 #'stringp 
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.")
@@ -2015,6 +2021,48 @@ SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend"
                     (gnosis-update 'notes `(= ,field ',value) `(= id ,id)))
                    (t (gnosis-update 'notes `(= ,field ,value) `(= id ,id))))))
 
+(defun gnosis-validate-custom-values (new-value)
+  "Validate the structure and values of NEW-VALUE for gnosis-custom-values."
+  (unless (listp new-value)
+    (error "GNOSIS-CUSTOM-VALUES should be a list of entries"))
+  (dolist (entry new-value)
+    (unless (and (listp entry) (= (length entry) 3)
+                 (memq (nth 0 entry) '(:deck :tag))
+                 (stringp (nth 1 entry))
+                 (listp (nth 2 entry))) ; Ensure the third element is a plist
+      (error "Each entry should a :deck or :tag keyword, a string, and a plist 
of custom values"))
+    (let ((proto (plist-get (nth 2 entry) :proto))
+          (anagnosis (plist-get (nth 2 entry) :anagnosis))
+          (epignosis (plist-get (nth 2 entry) :epignosis))
+          (agnoia (plist-get (nth 2 entry) :agnoia))
+          (amnesia (plist-get (nth 2 entry) :amnesia))
+          (lethe (plist-get (nth 2 entry) :lethe)))
+      (unless (listp proto)
+        (error "Proto must be a list of interval integer values"))
+      (unless (or (null anagnosis) (integerp anagnosis))
+        (error "Anagnosis should be an integer"))
+      (unless (or (null epignosis) (numberp epignosis))
+        (error "Epignosis should be a number"))
+      (unless (or (null agnoia) (numberp agnoia))
+        (error "Agnoia should be a number"))
+      (unless (or (null amnesia) (and (numberp amnesia) (<= amnesia 1) (>= 
amnesia 0)))
+        (error "Amnesia should be a number between 0 and 1"))
+      (unless (or (null lethe) (and (integerp lethe) (> lethe 0)))
+        (error "Lethe should be an integer greater than 0")))))
+
+(defun gnosis-custom-values-watcher (symbol new-value _operation _where)
+  "Watcher for gnosis custom values.
+
+SYMBOL to watch changes for.
+NEW-VALUE is the new value set to the variable.
+OPERATION is the type of operation being performed.
+WHERE is the buffer or object where the change happens."
+  (when (eq symbol 'gnosis-custom-values)
+    (gnosis-validate-custom-values new-value)))
+
+(add-variable-watcher 'gnosis-custom-values 'gnosis-custom-values-watcher)
+
+;; Validate custom values during review process as well.
 (defun gnosis-get-custom-values--validate (plist valid-keywords)
   "Verify that PLIST consists of VALID-KEYWORDS."
   (let ((keys (let (ks)
@@ -2251,7 +2299,7 @@ Defaults to current date."
   (let* ((date (or date (gnosis-algorithm-date)))
         (reviewed-new (or (car (gnosis-select 'reviewed-new 'activity-log `(= 
date ',date) t)) 0)))
     reviewed-new))
-;; TODO: Auto tag overdue tags
+;; TODO: Auto tag overdue tags.
 (defun gnosis-tags--append (id tag)
   "Append TAG to the list of tags of note ID."
   (cl-assert (numberp id) nil "ID must be the note id number")
@@ -2500,7 +2548,7 @@ If STRING-SECTION is nil, apply FACE to the entire 
STRING."
               (gnosis-add-note--cloze :deck deck-name
                                       :note "GNU Emacs is an extensible editor 
created by {{c1::Richard}} {{c1::Stallman}} in {{c2::1984::year}}"
                                       :tags note-tags
-                                      :extra "Emacs was originally implemented 
in 1976 on the MIT AI Lab's Incompatible Timesharing System (ITS), as a 
collection of TECO macros.  The name “Emacs” was originally chosen as an 
abbreviation of “Editor MACroS”. =This version of Emacs=, GNU Emacs, was 
originally *written in 1984*")
+                                      :extra "Emacs was originally implemented 
in 1976 on the MIT AI Lab's Incompatible Timesharing System (ITS), as a 
collection of TECO macros.  The name “Emacs” was originally chosen as an 
abbreviation of “Editor MACroS”. This version of Emacs, =GNU= =Emacs=, was 
originally written in _1984_")
               (gnosis-add-note--y-or-n :deck deck-name
                                        :question "Is GNU Emacs the 
unparalleled pinnacle of all software creation?"
                                        :hint "Duh"
@@ -2509,6 +2557,18 @@ If STRING-SECTION is nil, apply FACE to the entire 
STRING."
                                        :tags note-tags))
       (error "Demo deck already exists"))))
 
+;; TODO: Add Export funcs
+;; (defun gnosis-export-deck (&optional deck)
+;;   "Export contents of DECK."
+;;   (interactive (list (gnosis--get-deck-id)))
+;;   (with-current-buffer (get-buffer-create "*test*")
+;;     (insert (format "#+GNOSIS_DECK: %s\n\n" (gnosis--get-deck-name deck)))
+;;     (cl-loop for note in (gnosis-select '[main answer id type] 'notes `(= 
deck-id ,deck))
+;;          do (gnosis-org-insert-heading :main (car note)
+;;                                        :answer (cadr note)
+;;                                        :id (number-to-string (caddr note))
+;;                                        :type (cadddr note)))))
+
 ;; Gnosis mode ;;
 ;;;;;;;;;;;;;;;;;
 
diff --git a/manifest.scm b/manifest.scm
new file mode 100644
index 0000000000..ce98337fa0
--- /dev/null
+++ b/manifest.scm
@@ -0,0 +1,3 @@
+;;
+(specifications->manifest
+  (list "make" "texinfo" "emacs" "emacs-org"))

Reply via email to