summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--org-fc-awk.el2
-rw-r--r--org-fc-type-cloze.el211
-rw-r--r--org-fc-type-double.el73
-rw-r--r--org-fc-type-normal.el65
-rw-r--r--org-fc-type-text-input.el109
-rw-r--r--org-fc-type-vocab.el126
-rw-r--r--org-fc.el433
7 files changed, 589 insertions, 430 deletions
diff --git a/org-fc-awk.el b/org-fc-awk.el
index 6e23b31..9c9d89b 100644
--- a/org-fc-awk.el
+++ b/org-fc-awk.el
@@ -38,7 +38,7 @@ With the '-L' option, 'find' follows symlinks."
`(("fc_tag" . ,org-fc-flashcard-tag)
("suspended_tag" . ,org-fc-suspended-tag)
("type_property" . ,org-fc-type-property)
- ("cloze_type_property" . ,org-fc-cloze-type-property)
+ ("cloze_type_property" . ,org-fc-type-cloze-type-property)
("created_property" . ,org-fc-created-property)
("review_data_drawer" . ,org-fc-review-data-drawer)))
diff --git a/org-fc-type-cloze.el b/org-fc-type-cloze.el
new file mode 100644
index 0000000..5774d8b
--- /dev/null
+++ b/org-fc-type-cloze.el
@@ -0,0 +1,211 @@
+;;; org-fc-type-cloze.el --- Cloze deletion card type -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 Leon Rische
+
+;; Author: Leon Rische <emacs@leonrische.me>
+;; Url: https://www.leonrische.me/pages/org_flashcards.html
+;; Package-requires: ((emacs "26.3") (org "9.3"))
+;; Version: 0.0.1
+
+;; 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:
+;;
+;;; Code:
+
+(defcustom org-fc-type-cloze-type-property "FC_CLOZE_TYPE"
+ "Property used to store the card's subtype for cloze cards."
+ :type 'string
+ :group 'org-fc)
+
+(defcustom org-fc-type-cloze-context 1
+ "Number of surrounding cards to show for 'context' type cards."
+ :type 'number
+ :group 'org-fc)
+
+(defface org-fc-type-cloze-hole-face
+ '((t (:bold t)))
+ "Face for org-fc cloze card holes."
+ :group 'org-fc)
+
+(defvar org-fc-type-cloze-types
+ '(deletion enumeration context single)
+ "List of valid cloze card subtypes.")
+
+(defvar org-fc-type-cloze--text '()
+ "Text overlay.")
+(defvar org-fc-type-cloze--hint '()
+ "Hint overlay.")
+
+;;; Hole Parsing / Hiding
+
+(defvar org-fc-type-cloze-hole-re
+ (rx
+ (seq
+ "{{"
+ (group-n 1 (* (or (seq "$" (+ (not (any "$"))) "$")
+ (not (any "}"))))) "}"
+ (? (seq "{" (group-n 2 (* (or (seq "$" (not (any "$")) "$")
+ (not (any "}"))))) "}"))
+ (? "@" (group-n 3 (+ digit)))
+ "}"))
+ "Regexp for a cloze holes.")
+
+(defun org-fc-type-cloze-max-hole-id ()
+ "Get the max-hole property of the heading at point."
+ (if-let ((max-id (org-entry-get (point) org-fc-type-cloze-max-hole-property)))
+ (string-to-number max-id)
+ -1))
+
+(defun org-fc-type-cloze--parse-holes (current-position end)
+ "Starting at point, collect all cloze holes before END.
+CURRENT-POSITION is the id of the hole being reviewed. Returns a
+pair (holes . current-index) where current-index is the index of
+the hole for the current position."
+ (let (holes current-index)
+ (while (re-search-forward org-fc-type-cloze-hole-re end t)
+ (when (match-beginning 3)
+ (push (match-data) holes)
+ (if (= current-position (string-to-number (match-string 3)))
+ (setq current-index (1- (length holes))))))
+ (cons (reverse holes) current-index)))
+
+(defun org-fc-type-cloze--hole-visible-p (type i current-index)
+ "Determine whether hole I of card TYPE should be visible based.
+CURRENT-INDEX is the index of the current position in the list of all holes."
+ (cl-case type
+ ('enumeration (< i current-index))
+ ('deletion t)
+ ('single nil)
+ ('context (<= (abs (- i current-index)) org-fc-type-cloze-context))
+ (t (error "Org-fc: Unknown cloze card type %s" type))))
+
+(defun org-fc-type-cloze--end ()
+ "End of contents of heading at point, excluding subheadings."
+ (save-excursion
+ ;; If there is no next heading, we end up at `(point-max)`
+ (outline-next-heading)
+ (1- (point))))
+
+(defun org-fc-type-cloze-hide-holes (position)
+ "Hide holes of a card of TYPE in relation to POSITION."
+ (org-fc-with-point-at-entry
+ (let* ((type (intern (org-entry-get (point) org-fc-type-cloze-type-property)))
+ (end (1+ (org-fc-type-cloze--end)))
+ (holes-index (org-fc-type-cloze--parse-holes position end))
+ (holes (car holes-index))
+ (current-index (cdr holes-index)))
+ (cl-loop
+ for i below (length holes)
+ for (hole-beg hole-end text-beg text-end hint-beg hint-end) in holes
+ do
+ (progn
+ ;; Fake position if there is no hint
+ (unless hint-beg (setq hint-beg text-end))
+ (unless hint-end (setq hint-end text-end))
+ (cond
+ ;; If the hole is the one currently being reviewed, hide all
+ ;; the hole markup, hide the answer, format the hint as
+ ;; "[...hint]" and set the font for the whole hole.
+ ((= i current-index)
+ (org-fc-hide-region hole-beg text-beg "")
+ (remove-overlays text-beg text-end)
+ (setq org-fc-type-cloze--text
+ (org-fc-make-overlay text-beg text-end 'invisible t))
+ (org-fc-hide-region text-end hint-beg "")
+ (setq org-fc-type-cloze--hint
+ (org-fc-overlay-surround
+ (org-fc-make-overlay hint-beg hint-end)
+ "[..." "]" 'org-fc-type-cloze-hole-face))
+ (org-fc-hide-region hint-end hole-end "")
+ (org-fc-make-overlay
+ hole-beg hole-end
+ 'face 'org-fc-type-cloze-hole-face))
+ ;; If the text of another hole should be visible,
+ ;; hide the hole markup and the hint
+ ((org-fc-type-cloze--hole-visible-p type i current-index)
+ (org-fc-hide-region hole-beg text-beg)
+ (org-fc-hide-region text-end hole-end))
+ ;; If the text of another hole should not be visible,
+ ;; hide the whole hole
+ (t (org-fc-hide-region hole-beg hole-end "..."))))))))
+
+;;; Setup / Flipping
+
+(defun org-fc-type-cloze-init (type)
+ "Initialize the current heading for use as a cloze card of subtype TYPE.
+Processes all holes in the card text."
+ (interactive (list
+ (intern
+ (completing-read "Cloze Type: " org-fc-type-cloze-types))))
+ (unless (member type org-fc-type-cloze-types)
+ (error "Invalid cloze card type: %s" type))
+ (org-fc--init-card "cloze")
+ (org-fc-type-cloze-update)
+ (org-set-property org-fc-type-cloze-type-property (format "%s" type)))
+
+(defun org-fc-type-cloze-setup (position)
+ "Prepare POSITION of a cloze card for review."
+ (setq org-fc-type-cloze--text nil)
+ (setq org-fc-type-cloze--hint nil)
+ (outline-hide-subtree)
+ (org-show-entry)
+ (org-fc-type-cloze-hide-holes (string-to-number position)))
+
+(defun org-fc-type-cloze-flip ()
+ "Flip a cloze card."
+ (org-show-children)
+ (overlay-put org-fc-type-cloze--text 'invisible nil)
+ (org-fc-show-latex)
+ ;; Remove all overlays in the region of the hint to get rid of
+ ;; latex overlays in the hint, then hide the region again.
+ (let* ((hint-start (overlay-start org-fc-type-cloze--hint))
+ (hint-end (overlay-end org-fc-type-cloze--hint)))
+ (remove-overlays hint-start hint-end)
+ (org-fc-hide-region hint-start hint-end)))
+
+(defun org-fc-type-cloze-update ()
+ "Update the review data & deletions of the current heading."
+ (let* ((end (org-fc-type-cloze--end))
+ (hole-id (1+ (org-fc-type-cloze-max-hole-id)))
+ ids)
+ (save-excursion
+ (while (re-search-forward org-fc-type-cloze-hole-re end t)
+ (let ((id (match-string 3))
+ (hole-end (match-end 0)))
+ (unless id
+ (setq id hole-id)
+ (cl-incf hole-id 1)
+ (let ((id-str (number-to-string id)))
+ (cl-incf end (+ 1 (length id-str)))
+ (goto-char hole-end)
+ (backward-char)
+ (insert "@" id-str)))
+ (push (format "%s" id) ids))))
+ (org-set-property
+ org-fc-type-cloze-max-hole-property
+ (format "%s" (1- hole-id)))
+ (org-fc-review-data-update (reverse ids))))
+
+(org-fc-register-type
+ 'cloze
+ 'org-fc-type-cloze-setup
+ 'org-fc-type-cloze-flip
+ 'org-fc-type-cloze-update)
+
+;;; Footer
+
+(provide 'org-fc-type-cloze)
+
+;;; org-fc-type-cloze.el ends here
diff --git a/org-fc-type-double.el b/org-fc-type-double.el
new file mode 100644
index 0000000..12ecb8f
--- /dev/null
+++ b/org-fc-type-double.el
@@ -0,0 +1,73 @@
+;;; org-fc-type-double.el --- Front <-> Back Card Type -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 Leon Rische
+
+;; Author: Leon Rische <emacs@leonrische.me>
+;; Url: https://www.leonrische.me/pages/org_flashcards.html
+;; Package-requires: ((emacs "26.3") (org "9.3"))
+;; Version: 0.0.1
+
+;; 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:
+;;
+;; Variant of the normal card type that's also reviewed in the inverse
+;; (back -> front) direction.
+;;
+;;; Code:
+
+(defvar org-fc-type-double--overlay '())
+
+(defun org-fc-type-double-init ()
+ "Mark headline as card of the double type."
+ (interactive)
+ (org-fc--init-card "double")
+ (org-fc-review-data-update '("front" "back")))
+
+(defun org-fc-type-double-setup (position)
+ "Prepare POSITION of a double card for review."
+ (pcase position
+ ("front" (org-fc-type-normal-setup position))
+ ("back"
+ (outline-hide-subtree)
+ (if (org-fc-has-back-heading-p)
+ (org-fc-with-point-at-back-heading
+ (org-fc-show-latex)
+ (outline-show-entry))
+ (org-show-entry)
+ (setq org-fc-type-double--overlay (org-fc-hide-heading "[...]"))))
+ (_ (error "Invalid double position %s" position))))
+
+(defun org-fc-type-double-flip ()
+ "Flip a double card."
+ (if org-fc-type-double--overlay
+ (delete-overlay org-fc-type-double--overlay))
+ (org-show-entry)
+ (org-show-children)
+ (org-fc-with-point-at-back-heading
+ (org-show-entry)
+ (org-show-children)
+ (org-fc-show-latex)))
+
+(org-fc-register-type
+ 'double
+ 'org-fc-type-double-setup
+ 'org-fc-type-double-flip
+ 'org-fc-noop)
+
+;;; Footer
+
+(provide 'org-fc-type-double)
+
+;;; org-fc-type-double.el ends here
diff --git a/org-fc-type-normal.el b/org-fc-type-normal.el
new file mode 100644
index 0000000..9dddf14
--- /dev/null
+++ b/org-fc-type-normal.el
@@ -0,0 +1,65 @@
+;;; org-fc-type-normal.el --- Front -> Back Card Type -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 Leon Rische
+
+;; Author: Leon Rische <emacs@leonrische.me>
+;; Url: https://www.leonrische.me/pages/org_flashcards.html
+;; Package-requires: ((emacs "26.3") (org "9.3"))
+;; Version: 0.0.1
+
+;; 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:
+;;
+;;; Code:
+
+(defun org-fc-type-normal-init ()
+ "Mark headline as card of the normal type."
+ (interactive)
+ (org-fc--init-card "normal")
+ (org-fc-review-data-update '("front")))
+
+(defun org-fc-type-normal-setup (_position)
+ "Prepare a normal card for review."
+ (interactive)
+ ;; Make sure the card is collapsed
+ (outline-hide-subtree)
+ (when (org-fc-has-back-heading-p)
+ (org-show-entry)
+ ;; Make sure the back heading is visible
+ (org-fc-with-point-at-back-heading
+ (org-show-set-visibility 'minimal))))
+
+(defun org-fc-type-normal-flip ()
+ "Flip a normal card."
+ (interactive)
+ (org-show-entry)
+ (org-show-children)
+ ;; NOTE: the body only runs if the card has a back heading
+ (org-fc-with-point-at-back-heading
+ (org-show-entry)
+ (org-show-children)
+ (org-fc-show-latex)))
+
+(org-fc-register-type
+ 'normal
+ 'org-fc-type-normal-setup
+ 'org-fc-type-normal-flip
+ 'org-fc-noop)
+
+;;; Footer
+
+(provide 'org-fc-type-normal)
+
+;;; org-fc-type-normal.el ends here
diff --git a/org-fc-type-text-input.el b/org-fc-type-text-input.el
new file mode 100644
index 0000000..6d8430c
--- /dev/null
+++ b/org-fc-type-text-input.el
@@ -0,0 +1,109 @@
+;;; org-fc-type-text-input.el --- Text-input card type -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 Leon Rische
+
+;; Author: Leon Rische <emacs@leonrische.me>
+;; Url: https://www.leonrische.me/pages/org_flashcards.html
+;; Package-requires: ((emacs "26.3") (org "9.3"))
+;; Version: 0.0.1
+
+;; 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:
+;;
+;;; Code:
+
+(defun org-fc-text-input-content ()
+ "Return the first line of a cards (back) contents.
+Returns a pair (pos . string). If the card has a 'Back' heading,
+its content is used, otherwise, the main content is used. This
+function is expected to be called with point on a heading."
+ (save-excursion
+ ;; Go to main or "Back" heading
+ (if-let ((pos (org-fc-back-heading-position)))
+ (goto-char pos))
+
+ ;; Skip metadata & drawers
+ (forward-line)
+ (while (or (looking-at-p org-scheduled-regexp)
+ (looking-at-p org-deadline-regexp))
+ (forward-line))
+ (while (looking-at org-drawer-regexp)
+ (if (re-search-forward ":END:" nil t)
+ (forward-line)
+ (error "No :END: found for drawer")))
+ (unless (looking-at-p org-heading-regexp)
+ (cons
+ (point)
+ (buffer-substring-no-properties (point) (point-at-eol))))))
+
+(defun org-fc-type-text-input-init ()
+ "Mark headline as card of the text-input type."
+ (interactive)
+ (unless (org-fc-text-input-content)
+ (error "Card contains content"))
+ (org-fc--init-card "text-input")
+ (org-fc-review-data-update '("front")))
+
+(defun org-fc-type-text-input-setup (_position)
+ "Prepare a text-input card for review."
+ (interactive)
+ ;; Hide answer
+ (outline-hide-subtree)
+ (when (org-fc-has-back-heading-p)
+ (org-show-entry)
+ (org-fc-with-point-at-back-heading (org-show-set-visibility 'minimal)))
+ ;; Prompt user, create diff overlay
+ (let* ((pos-content (org-fc-text-input-content))
+ (content (cdr pos-content))
+ (start (car pos-content))
+ (end (+ start (length content)))
+ (deemph (org-fc-deemphasize content))
+ (diff (org-fc-diff (read-string "Answer: ") (cdr deemph))))
+ ;; Overlay for user input
+ (when (car deemph)
+ (setq start (1+ start))
+ (setq end (1- end)))
+ (org-fc-hide-region start end (car diff))
+ ;; Overlay for expected answer, using the newline after the answer
+ (if (cdr diff)
+ (org-fc-hide-region
+ end (1+ end)
+ (concat
+ " (expected: "
+ (if (null (car deemph))
+ (cdr diff)
+ (org-fc-emphasize
+ (concat (car deemph) (cdr diff) (car deemph))))
+ ")\n"))))
+ ;; Reveal answer & diff
+ (save-excursion
+ (org-show-entry)
+ (org-show-children)
+ (org-fc-with-point-at-back-heading
+ (org-show-entry)
+ (org-show-children)
+ (org-fc-show-latex))))
+
+(org-fc-register-type
+ 'text-input
+ 'org-fc-type-text-input-setup
+ nil
+ 'org-fc-noop)
+
+;;; Footer
+
+(provide 'org-fc-type-text-input)
+
+;;; org-fc-type-text-input.el ends here
diff --git a/org-fc-type-vocab.el b/org-fc-type-vocab.el
new file mode 100644
index 0000000..f278049
--- /dev/null
+++ b/org-fc-type-vocab.el
@@ -0,0 +1,126 @@
+;;; org-fc-type-vocab.el --- Card type for learning vocabulary -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020 Leon Rische
+
+;; Author: Leon Rische <emacs@leonrische.me>
+;; Url: https://www.leonrische.me/pages/org_flashcards.html
+;; Package-requires: ((emacs "26.3") (org "9.3"))
+;; Version: 0.0.1
+
+;; 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:
+;;
+;; Cards of this type should have the (foreign language) word as their
+;; heading with a definition on the back.
+;;
+;; During review, the user is prompted for the definition of the word
+;; or asked to type in the word based on its definitions.
+;;
+;;; Code:
+
+(require 'org-fc-audio)
+
+(defcustom org-fc-type-vocab-slow-speed 0.7
+ "Speed to use for slow playback."
+ :type 'number
+ :group 'org-fc)
+
+(defcustom org-fc-type-vocab-audio-property "FC_VOCAB_AUDIO"
+ "Property with path to audio file."
+ :type 'string
+ :group 'org-fc)
+
+(defun org-fc-type-vocab-init ()
+ "Mark headline as card of the vocab type."
+ (interactive)
+ (org-fc--init-card "vocab")
+ (org-fc-review-data-update '("front" "back")))
+
+(defun org-fc-type-vocab-setup (position)
+ "Prepare POSITION of a vocab card for review."
+ (pcase position
+ ("front"
+ (org-fc-audio-play org-fc-type-vocab-audio-property)
+ (org-fc-type-normal-setup position))
+ ("back"
+ (org-fc-type-vocab-typing-setup)
+ (org-fc-audio-play org-fc-type-vocab-audio-property)
+ 'rate)
+ (_ (error "Invalid vocab position %s" position))))
+
+(defun org-fc-type-vocab-flip ()
+ "Flip a vocab card."
+ (org-fc-type-normal-flip))
+
+(defun org-fc-type-vocab-play ()
+ "Play vocab audio file at normal speed."
+ (interactive)
+ (org-fc-audio-play org-fc-type-vocab-audio-property))
+
+(defun org-fc-type-vocab-play-slow ()
+ "Play vocab audio file at slow speed."
+ (interactive)
+ (org-fc-audio-play org-fc-type-vocab-audio-property org-fc-type-vocab-slow-speed))
+
+(defun org-fc-vocab-content ()
+ "Heading position & text as a (pos . string) pair."
+ (save-excursion
+ (org-fc-goto-entry-heading)
+ (let ((case-fold-search nil))
+ (if (looking-at org-complex-heading-regexp)
+ (cons
+ (match-beginning 4)
+ (buffer-substring-no-properties (match-beginning 4) (match-end 4)))))))
+
+(defun org-fc-type-vocab-typing-setup ()
+ "Prepare a text-input vocab card for review."
+ (interactive)
+ (org-show-subtree)
+ (let* ((pos-content (org-fc-vocab-content))
+ (content (cdr pos-content))
+ (start (car pos-content))
+ (end (+ start (length content)))
+ (ov (org-fc-hide-region start end "..."))
+ (deemph (org-fc-deemphasize content))
+ (diff (org-fc-diff (read-string "Answer: ") (cdr deemph))))
+ (delete-overlay ov)
+ ;; Overlay for user input
+ (when (car deemph)
+ (setq start (1+ start))
+ (setq end (1- end)))
+ (org-fc-hide-region start end (car diff))
+ ;; Overlay for expected answer, using the newline after the answer
+ (if (cdr diff)
+ (org-fc-hide-region
+ end (1+ end)
+ (concat
+ "\n! "
+ (if (null (car deemph))
+ (cdr diff)
+ (org-fc-emphasize
+ (concat (car deemph) (cdr diff) (car deemph))))
+ "")))))
+
+(org-fc-register-type
+ 'vocab
+ 'org-fc-type-vocab-setup
+ 'org-fc-type-vocab-flip
+ 'org-fc-noop)
+
+;;; Footer
+
+(provide 'org-fc-type-vocab)
+
+;;; org-fc-type-vocab.el ends here
diff --git a/org-fc.el b/org-fc.el
index 241acab..f775d2c 100644
--- a/org-fc.el
+++ b/org-fc.el
@@ -78,11 +78,6 @@ Used to generate absolute paths to the awk scripts.")
:type 'string
:group 'org-fc)
-(defcustom org-fc-cloze-type-property "FC_CLOZE_TYPE"
- "Property used to store the card's subtype for cloze cards."
- :type 'string
- :group 'org-fc)
-
(defcustom org-fc-created-property "FC_CREATED"
"Property used to store the cards creation time."
:type 'string
@@ -93,11 +88,6 @@ Used to generate absolute paths to the awk scripts.")
:type 'string
:group 'org-fc)
-(defcustom org-fc-type-cloze-type-property "FC_CLOZE_TYPE"
- "Name of the property to use for storing the cloze subtype."
- :type 'string
- :group 'org-fc)
-
(defcustom org-fc-suspended-tag "suspended"
"Tag for marking suspended cards."
:type 'string
@@ -235,11 +225,6 @@ Does not apply to cloze single and cloze enumeration cards."
;;;; Font Faces
-(defface org-fc-type-cloze-hole-face
- '((t (:bold t)))
- "Face for org-fc cloze card holes."
- :group 'org-fc)
-
;; Based on `magit-diff-added'
(defface org-fc-diff-correct
`((((class color) (background light))
@@ -599,420 +584,10 @@ Argument UPDATE-FN Function to update a card when it's contents have changed."
(cl-third entry)
(error "No such flashcard type: %s" type))))
-;;;; Normal
-
-(defun org-fc-type-normal-init ()
- "Mark headline as card of the normal type."
- (interactive)
- (org-fc--init-card "normal")
- (org-fc-review-data-update '("front")))
-
-(defun org-fc-type-normal-setup (_position)
- "Prepare a normal card for review."
- (interactive)
- ;; Make sure the card is collapsed
- (outline-hide-subtree)
- (when (org-fc-has-back-heading-p)
- (org-show-entry)
- ;; Make sure the back heading is visible
- (org-fc-with-point-at-back-heading
- (org-show-set-visibility 'minimal))))
-
-(defun org-fc-type-normal-flip ()
- "Flip a normal card."
- (interactive)
- (org-show-entry)
- (org-show-children)
- ;; NOTE: the body only runs if the card has a back heading
- (org-fc-with-point-at-back-heading
- (org-show-entry)
- (org-show-children)
- (org-fc-show-latex)))
-
-(org-fc-register-type
- 'normal
- 'org-fc-type-normal-setup
- 'org-fc-type-normal-flip
- 'org-fc-noop)
-
-;;;; Double
-
-(defvar org-fc-type-double--overlay '())
-
-(defun org-fc-type-double-init ()
- "Mark headline as card of the double type."
- (interactive)
- (org-fc--init-card "double")
- (org-fc-review-data-update '("front" "back")))
-
-(defun org-fc-type-double-setup (position)
- "Prepare POSITION of a double card for review."
- (pcase position
- ("front" (org-fc-type-normal-setup position))
- ("back"
- (outline-hide-subtree)
- (if (org-fc-has-back-heading-p)
- (org-fc-with-point-at-back-heading
- (org-fc-show-latex)
- (outline-show-entry))
- (org-show-entry)
- (setq org-fc-type-double--overlay (org-fc-hide-heading "[...]"))))
- (_ (error "Invalid double position %s" position))))
-
-(defun org-fc-type-double-flip ()
- "Flip a double card."
- (if org-fc-type-double--overlay
- (delete-overlay org-fc-type-double--overlay))
- (org-show-entry)
- (org-show-children)
- (org-fc-with-point-at-back-heading
- (org-show-entry)
- (org-show-children)
- (org-fc-show-latex)))
-
-(org-fc-register-type
- 'double
- 'org-fc-type-double-setup
- 'org-fc-type-double-flip
- 'org-fc-noop)
-
-;;;; Text-Input
-
-(defun org-fc-text-input-content ()
- "Return the first line of a cards (back) contents.
-Returns a pair (pos . string). If the card has a 'Back' heading,
-its content is used, otherwise, the main content is used. This
-function is expected to be called with point on a heading."
- (save-excursion
- ;; Go to main or "Back" heading
- (if-let ((pos (org-fc-back-heading-position)))
- (goto-char pos))
-
- ;; Skip metadata & drawers
- (forward-line)
- (while (or (looking-at-p org-scheduled-regexp)
- (looking-at-p org-deadline-regexp))
- (forward-line))
- (while (looking-at org-drawer-regexp)
- (if (re-search-forward ":END:" nil t)
- (forward-line)
- (error "No :END: found for drawer")))
- (unless (looking-at-p org-heading-regexp)
- (cons
- (point)
- (buffer-substring-no-properties (point) (point-at-eol))))))
-
-(defun org-fc-type-text-input-init ()
- "Mark headline as card of the text-input type."
- (interactive)
- (unless (org-fc-text-input-content)
- (error "Card contains content"))
- (org-fc--init-card "text-input")
- (org-fc-review-data-update '("front")))
-
-(defun org-fc-type-text-input-setup (_position)
- "Prepare a text-input card for review."
- (interactive)
- ;; Hide answer
- (outline-hide-subtree)
- (when (org-fc-has-back-heading-p)
- (org-show-entry)
- (org-fc-with-point-at-back-heading (org-show-set-visibility 'minimal)))
- ;; Prompt user, create diff overlay
- (let* ((pos-content (org-fc-text-input-content))
- (content (cdr pos-content))
- (start (car pos-content))
- (end (+ start (length content)))
- (deemph (org-fc-deemphasize content))
- (diff (org-fc-diff (read-string "Answer: ") (cdr deemph))))
- ;; Overlay for user input
- (when (car deemph)
- (setq start (1+ start))
- (setq end (1- end)))
- (org-fc-hide-region start end (car diff))
- ;; Overlay for expected answer, using the newline after the answer
- (if (cdr diff)
- (org-fc-hide-region
- end (1+ end)
- (concat
- " (expected: "
- (if (null (car deemph))
- (cdr diff)
- (org-fc-emphasize
- (concat (car deemph) (cdr diff) (car deemph))))
- ")\n"))))
- ;; Reveal answer & diff
- (save-excursion
- (org-show-entry)
- (org-show-children)
- (org-fc-with-point-at-back-heading
- (org-show-entry)
- (org-show-children)
- (org-fc-show-latex))))
-
-(org-fc-register-type
- 'text-input
- 'org-fc-type-text-input-setup
- nil
- 'org-fc-noop)
-
-;;;; Vocab
-
-(defcustom org-fc-type-vocab-slow-speed 0.7
- "Speed to use for slow playback.")
-
-(defcustom org-fc-type-vocab-audio-property "FC_VOCAB_AUDIO"
- "Property with path to audio file.")
-
-(defun org-fc-type-vocab-init ()
- "Mark headline as card of the vocab type."
- (interactive)
- (org-fc--init-card "vocab")
- (org-fc-review-data-update '("front" "back")))
-
-(defun org-fc-type-vocab-setup (position)
- "Prepare POSITION of a vocab card for review."
- (pcase position
- ("front"
- (org-fc-audio-play org-fc-type-vocab-audio-property)
- (org-fc-type-normal-setup position))
- ("back"
- (org-fc-type-vocab-typing-setup)
- (org-fc-audio-play org-fc-type-vocab-audio-property)
- 'rate)
- (_ (error "Invalid vocab position %s" position))))
-
-(defun org-fc-type-vocab-flip ()
- "Flip a vocab card."
- (org-fc-type-normal-flip))
-
-(defun org-fc-type-vocab-play ()
- "Play vocab audio file at normal speed."
- (interactive)
- (org-fc-audio-play org-fc-type-vocab-audio-property))
-
-(defun org-fc-type-vocab-play-slow ()
- "Play vocab audio file at slow speed."
- (interactive)
- (org-fc-audio-play org-fc-type-vocab-audio-property org-fc-type-vocab-slow-speed))
-
-(defun org-fc-vocab-content ()
- "Heading position & text as a (pos . string) pair."
- (save-excursion
- (org-fc-goto-entry-heading)
- (let ((case-fold-search nil))
- (if (looking-at org-complex-heading-regexp)
- (cons
- (match-beginning 4)
- (buffer-substring-no-properties (match-beginning 4) (match-end 4)))))))
-
-(defun org-fc-type-vocab-typing-setup ()
- "Prepare a text-input vocab card for review."
- (interactive)
- (org-show-subtree)
- (let* ((pos-content (org-fc-vocab-content))
- (content (cdr pos-content))
- (start (car pos-content))
- (end (+ start (length content)))
- (ov (org-fc-hide-region start end "..."))
- (deemph (org-fc-deemphasize content))
- (diff (org-fc-diff (read-string "Answer: ") (cdr deemph))))
- (delete-overlay ov)
- ;; Overlay for user input
- (when (car deemph)
- (setq start (1+ start))
- (setq end (1- end)))
- (org-fc-hide-region start end (car diff))
- ;; Overlay for expected answer, using the newline after the answer
- (if (cdr diff)
- (org-fc-hide-region
- end (1+ end)
- (concat
- "\n! "
- (if (null (car deemph))
- (cdr diff)
- (org-fc-emphasize
- (concat (car deemph) (cdr diff) (car deemph))))
- "")))))
-
-(org-fc-register-type
- 'vocab
- 'org-fc-type-vocab-setup
- 'org-fc-type-vocab-flip
- 'org-fc-noop)
-
-;;;; Cloze
-
-;; NOTE: The context type is not implemented yet
-(defvar org-fc-type-cloze-types
- '(deletion enumeration context single)
- "List of valid cloze card subtypes.")
-
-(defvar org-fc-type-cloze--text '()
- "Text overlay.")
-(defvar org-fc-type-cloze--hint '()
- "Hint overlay.")
-
-(defcustom org-fc-type-cloze-context 1
- "Number of surrounding cards to show for 'context' type cards."
- :type 'number
- :group 'org-fc)
-
-;;;;; Hole Parsing / Hiding
-
-(defvar org-fc-type-cloze-hole-re
- (rx
- (seq
- "{{"
- (group-n 1 (* (or (seq "$" (+ (not (any "$"))) "$")
- (not (any "}"))))) "}"
- (? (seq "{" (group-n 2 (* (or (seq "$" (not (any "$")) "$")
- (not (any "}"))))) "}"))
- (? "@" (group-n 3 (+ digit)))
- "}"))
- "Regexp for a cloze holes.")
-
-(defun org-fc-type-cloze-max-hole-id ()
- "Get the max-hole property of the heading at point."
- (if-let ((max-id (org-entry-get (point) org-fc-type-cloze-max-hole-property)))
- (string-to-number max-id)
- -1))
-
-(defun org-fc-type-cloze--parse-holes (current-position end)
- "Starting at point, collect all cloze holes before END.
-CURRENT-POSITION is the id of the hole being reviewed. Returns a
-pair (holes . current-index) where current-index is the index of
-the hole for the current position."
- (let (holes current-index)
- (while (re-search-forward org-fc-type-cloze-hole-re end t)
- (when (match-beginning 3)
- (push (match-data) holes)
- (if (= current-position (string-to-number (match-string 3)))
- (setq current-index (1- (length holes))))))
- (cons (reverse holes) current-index)))
-
-(defun org-fc-type-cloze--hole-visible-p (type i current-index)
- "Determine whether hole I of card TYPE should be visible based.
-CURRENT-INDEX is the index of the current position in the list of all holes."
- (cl-case type
- ('enumeration (< i current-index))
- ('deletion t)
- ('single nil)
- ('context (<= (abs (- i current-index)) org-fc-type-cloze-context))
- (t (error "Org-fc: Unknown cloze card type %s" type))))
-
-(defun org-fc-type-cloze--end ()
- "End of contents of heading at point, excluding subheadings."
- (save-excursion
- ;; If there is no next heading, we end up at `(point-max)`
- (outline-next-heading)
- (1- (point))))
-
-(defun org-fc-type-cloze-hide-holes (position)
- "Hide holes of a card of TYPE in relation to POSITION."
- (org-fc-with-point-at-entry
- (let* ((type (intern (org-entry-get (point) org-fc-type-cloze-type-property)))
- (end (org-fc-type-cloze--end))
- (holes-index (org-fc-type-cloze--parse-holes position end))
- (holes (car holes-index))
- (current-index (cdr holes-index)))
- (cl-loop
- for i below (length holes)
- for (hole-beg hole-end text-beg text-end hint-beg hint-end) in holes
- do
- (progn
- ;; Fake position if there is no hint
- (unless hint-beg (setq hint-beg text-end))
- (unless hint-end (setq hint-end text-end))
- (cond
- ;; If the hole is the one currently being reviewed, hide all
- ;; the hole markup, hide the answer, format the hint as
- ;; "[...hint]" and set the font for the whole hole.
- ((= i current-index)
- (org-fc-hide-region hole-beg text-beg "")
- (remove-overlays text-beg text-end)
- (setq org-fc-type-cloze--text
- (org-fc-make-overlay text-beg text-end 'invisible t))
- (org-fc-hide-region text-end hint-beg "")
- (setq org-fc-type-cloze--hint
- (org-fc-overlay-surround
- (org-fc-make-overlay hint-beg hint-end)
- "[..." "]" 'org-fc-type-cloze-hole-face))
- (org-fc-hide-region hint-end hole-end "")
- (org-fc-make-overlay
- hole-beg hole-end
- 'face 'org-fc-type-cloze-hole-face))
- ;; If the text of another hole should be visible,
- ;; hide the hole markup and the hint
- ((org-fc-type-cloze--hole-visible-p type i current-index)
- (org-fc-hide-region hole-beg text-beg)
- (org-fc-hide-region text-end hole-end))
- ;; If the text of another hole should not be visible,
- ;; hide the whole hole
- (t (org-fc-hide-region hole-beg hole-end "..."))))))))
-
-;;;;; Setup / Flipping
-
-(defun org-fc-type-cloze-init (type)
- "Initialize the current heading for use as a cloze card of subtype TYPE.
-Processes all holes in the card text."
- (interactive (list
- (intern
- (completing-read "Cloze Type: " org-fc-type-cloze-types))))
- (unless (member type org-fc-type-cloze-types)
- (error "Invalid cloze card type: %s" type))
- (org-fc--init-card "cloze")
- (org-fc-type-cloze-update)
- (org-set-property org-fc-type-cloze-type-property (format "%s" type)))
-
-(defun org-fc-type-cloze-setup (position)
- "Prepare POSITION of a cloze card for review."
- (setq org-fc-type-cloze--text nil)
- (setq org-fc-type-cloze--hint nil)
- (outline-hide-subtree)
- (org-show-entry)
- (org-fc-type-cloze-hide-holes (string-to-number position)))
-
-(defun org-fc-type-cloze-flip ()
- "Flip a cloze card."
- (org-show-children)
- (overlay-put org-fc-type-cloze--text 'invisible nil)
- (org-fc-show-latex)
- ;; Remove all overlays in the region of the hint to get rid of
- ;; latex overlays in the hint, then hide the region again.
- (let* ((hint-start (overlay-start org-fc-type-cloze--hint))
- (hint-end (overlay-end org-fc-type-cloze--hint)))
- (remove-overlays hint-start hint-end)
- (org-fc-hide-region hint-start hint-end)))
-
-(defun org-fc-type-cloze-update ()
- "Update the review data & deletions of the current heading."
- (let* ((end (org-fc-type-cloze--end))
- (hole-id (1+ (org-fc-type-cloze-max-hole-id)))
- ids)
- (save-excursion
- (while (re-search-forward org-fc-type-cloze-hole-re end t)
- (let ((id (match-string 3))
- (hole-end (match-end 0)))
- (unless id
- (setq id hole-id)
- (cl-incf hole-id 1)
- (let ((id-str (number-to-string id)))
- (cl-incf end (+ 1 (length id-str)))
- (goto-char hole-end)
- (backward-char)
- (insert "@" id-str)))
- (push (format "%s" id) ids))))
- (org-set-property
- org-fc-type-cloze-max-hole-property
- (format "%s" (1- hole-id)))
- (org-fc-review-data-update (reverse ids))))
-
-(org-fc-register-type
- 'cloze
- 'org-fc-type-cloze-setup
- 'org-fc-type-cloze-flip
- 'org-fc-type-cloze-update)
+(require 'org-fc-type-normal)
+(require 'org-fc-type-double)
+(require 'org-fc-type-text-input)
+(require 'org-fc-type-cloze)
;;; Working with Overlays / Hiding Text
;;;; Showing / Hiding Overlays