Skip to content

Instantly share code, notes, and snippets.

@RyanFleck
Last active November 26, 2024 21:37
Show Gist options
  • Save RyanFleck/3195387af3ebd302152ec3397457689a to your computer and use it in GitHub Desktop.
Save RyanFleck/3195387af3ebd302152ec3397457689a to your computer and use it in GitHub Desktop.
Offline Scripture Text Insertion Functions - rcf-sword.el
;;; rcf-sword.el --- Use a local .bblx file to insert and read scripture.
;;; Commentary:
;; For scriptural study.
;; Eventually you should publish this: https://spin.atomicobject.com/write-emacs-package/
;; =====================================================================
;; Changelog - Make a quick note when you update this file.
;; =====================================================================
;; 2024-08-01 -> Wrote first version of this program, including
;; checking the sqlite db, reading the user input string, and
;; inserting an org-mode quote block into the buffer.
;; 2024-08-02 -> Cleaned up the code.
;; 2024-08-02 -> Added 'insert random verse' functionality.
;; 2024-09-10 -> Change position of verse-version insertion
;; 2024-09-23 -> Fixed a bug where 'string includes?' would pick
;; the wrong book during casual insertion of scripture.
;; 2024-11-11 -> Added Markdown and plaintext insertion support.
;;; Code:
(require 'cl-lib)
(require 's)
(when (not (and
(sqlite-available-p)
(not (version< emacs-version "29"))))
;; If you can read this, compile your emacs: https://ryanfleck.ca/2024/compiling-emacs/
(message "rcf-sword.el will not load correctly as it requires sqlite and Emacs 29."))
(defvar sword-bblx-file
"~/emacs/sword/new.king.james.version.bblx"
"Stores the path to the bblx format bible on disk.")
;; "~/emacs/sword/greek.interlinear.bblx"
(defvar sword-bblx-version-acroym
"NKJV"
"Stores the version acronym.")
(defvar sword-db
(sqlite-open (expand-file-name sword-bblx-file))
"Holds the database connection to the bible database on disk.")
;; The database must have the following format:
;; Typically shipped with '.bblx' extension.
;; Table: 'Bible' (can be viewed with sqlite3)
;; Book Chapter Verse Scripture
;; ----------------------------------------------------------------------------------------------------
;; 1 1 1 In the beginning God created the heavens and the earth.
;; 1 1 2 The earth was without form, and void; and darkness was {\cf2\super [1]} on ...
;; 1 1 3 Then God said, "Let there be light"; and there was light.
;; 1 1 4 And God saw the light, that it was good; and God divided the light from the...
;; Sorry to those who use more than 66 books - you'll have to hack this for yourself.
(defvar sword-book-data
'((1 1 "Genesis")
(1 2 "Exodus")
(1 3 "Leviticus")
(1 4 "Numbers")
(1 5 "Deuteronomy")
(1 6 "Joshua")
(1 7 "Judges")
(1 8 "Ruth")
(1 9 "1 Samuel")
(1 10 "2 Samuel")
(1 11 "1 Kings")
(1 12 "2 Kings")
(1 13 "1 Chronicles")
(1 14 "2 Chronicles")
(1 15 "Ezra")
(1 16 "Nehemiah")
(1 17 "Esther")
(1 18 "Job")
(1 19 "Psalms")
(1 20 "Proverbs")
(1 21 "Ecclesiastes")
(1 22 "Song of Solomon")
(1 23 "Isaiah")
(1 24 "Jeremiah")
(1 24 "Lamentations")
(1 26 "Ezekiel")
(1 27 "Daniel")
(1 28 "Hosea")
(1 29 "Joel")
(1 30 "Amos")
(1 31 "Obadiah")
(1 32 "Jonah")
(1 33 "Micah")
(1 34 "Nahum")
(1 35 "Habakkuk")
(1 36 "Zephaniah")
(1 37 "Haggai")
(1 38 "Zechariah")
(1 39 "Malachi")
(2 40 "Matthew")
(2 41 "Mark")
(2 42 "Luke")
(2 43 "John")
(2 44 "Acts")
(2 45 "Romans")
(2 46 "1 Corinthians")
(2 47 "2 Corinthians")
(2 48 "Galatians")
(2 49 "Ephesians")
(2 50 "Philippians")
(2 51 "Colossians")
(2 52 "1 Thessalonians")
(2 53 "2 Thessalonians")
(2 54 "1 Timothy")
(2 55 "2 Timothy")
(2 56 "Titus")
(2 57 "Philemon")
(2 58 "Hebrews")
(2 59 "James")
(2 60 "1 Peter")
(2 61 "2 Peter")
(2 62 "1 John")
(2 63 "2 John")
(2 64 "3 John")
(2 65 "Jude")
(2 66 "Revelation"))
"Enables a book number to be found from its name.")
;; (car sword-book-data) => (1 1 "Genesis")
;; (caddr (car sword-book-data)) => "Genesis"
;; What is a BOOK TRIPLE? It is the data above
;; Genesis chapter 1 is (1 1 "Genesis")
(defun book-title-contains? (book-triple str)
"Take the book data BOOK-TRIPLE and check if STR matches the title."
;; https://github.com/magnars/s.el?tab=readme-ov-file#s-contains-needle-s-optional-ignore-case
(s-contains? str (caddr book-triple) t))
(defun book-title-starts-with? (book-triple str)
"Take the book data BOOK-TRIPLE and check if STR matches the title."
;; https://github.com/magnars/s.el?tab=readme-ov-file#s-contains-needle-s-optional-ignore-case
(s-starts-with? str (caddr book-triple) t))
;; Example:
;; (check-book-title (car sword-book-data) "gen")
(defun get-book-data (bible-sword-book-data partial-book-str)
"Given a PARTIAL-BOOK-STR return the matching book in BIBLE-SWORD-BOOK-DATA."
(if-let
((book-data (seq-find #'(lambda (book-triple) (book-title-starts-with? book-triple partial-book-str)) bible-sword-book-data)))
book-data ;; If 'starts-with' finds the data return it, otherwise search with 'contains'.
(seq-find #'(lambda (book-triple) (book-title-contains? book-triple partial-book-str)) bible-sword-book-data)))
;; (get-book-data sword-book-data "Tim")
;; (get-book-data sword-book-data "Job")
(defun get-book-number (bible-sword-book-data str)
"Given a STR return the matching book in BIBLE-SWORD-BOOK-DATA."
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Sequence-Functions.html
(nth 1 (get-book-data bible-sword-book-data str)))
;; Last verse of last chapter:
;; (sqlite-execute sword-db "select MAX(chapter), MAX(verse) from bible where book=? and chapter=(SELECT MAX(chapter) FROM Bible WHERE book=?);" (list 2 2))
;; (sqlite-execute sword-db "SELECT MAX(chapter) FROM Bible WHERE book=?;" (list 2))
(defun get-random-book (bible-sword-data)
"Return a random book number using BIBLE-SWORD-DATA."
(+ 1 (random (length bible-sword-data))))
(defun get-random-chapter (bible-sword-data book)
"Return a BOOK number using BIBLE-SWORD-DATA."
(let ((max-chapter (or (caar (sqlite-execute sword-db "SELECT MAX(chapter) FROM Bible WHERE book=?;" (list book))) 0)))
(+ 1 (random max-chapter))))
(defun get-random-verse (bible-sword-data book chapter)
"Return a book number using BIBLE-SWORD-DATA in BOOK and CHAPTER."
(let ((max-verse (or (caar (sqlite-execute sword-db "SELECT MAX(verse) FROM Bible WHERE book=? AND chapter=?;" (list book chapter))) 0)))
(+ 1 (random max-verse))))
;; (get-random-book sword-book-data)
;; (get-random-chapter sword-book-data 2)
;; (get-random-chapter sword-book-data 200)
;; (get-random-verse sword-book-data 2 20)
(defun get-random-scripture-data (bible-books)
"Return a random verse as (book chapter verse) using BIBLE-BOOKS."
(let* ((book (get-random-book bible-books))
(chapter (get-random-chapter bible-books book))
(verse (get-random-verse bible-books book chapter)))
(list book chapter verse)))
(defun get-random-scripture (bible-books)
"Return a random verse using BIBLE-BOOKS."
(let* ((data (get-random-scripture-data bible-books))
(result (sqlite-execute sword-db "select * from bible where book=? and chapter=? and verse=?;" data)))
(if (or (not result) (equal result 0))
(message "I goofed up, data was %s" data)
result)))
(defun get-random-proverb-data (bible-books)
"Return a random verse as (book chapter verse) using BIBLE-BOOKS."
(let* ((book (get-book-number bible-books "Proverbs"))
(chapter (get-random-chapter bible-books book))
(verse (get-random-verse bible-books book chapter)))
(list book chapter verse)))
(defun get-random-proverb (bible-books)
"Return a random verse using BIBLE-BOOKS."
(let* ((data (get-random-proverb-data bible-books))
(result (sqlite-execute sword-db "select * from bible where book=? and chapter=? and verse=?;" data)))
(if (or (not result) (equal result 0))
(message "I goofed up, data was %s" data)
result)))
;; (get-random-scripture sword-book-data)
(defun get-book-name (bible-sword-book-data str)
"Given a STR return the matching book in BIBLE-SWORD-BOOK-DATA."
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Sequence-Functions.html
(nth 2 (get-book-data bible-sword-book-data str)))
(defun get-book-name-from-num (bible-sword-book-data num)
"Given a NUM return the matching book in BIBLE-SWORD-BOOK-DATA."
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Sequence-Functions.html
(nth 2 (seq-find #'(lambda (x) (equal (nth 1 x) num)) bible-sword-book-data)))
;; (get-book-name-from-num sword-book-data 4)
;; (get-book-name sword-book-data "Num")
;; Example:
;; (get-book-number sword-book-data "Mat") => (2 40 "Matthew")
(defun get-bible-data-book (book-number)
"Retrieves a full book from the bible as indicated by BOOK-NUMBER."
(sqlite-execute sword-db "select * from bible where book=?" (list book-number)))
;; (get-bible-data-book-chapter 46)
(defun get-bible-data-book-chapter (book-number book-chapter)
"With BOOK-NUMBER and BOOK-CHAPTER return data for one book of the bible."
(sqlite-execute sword-db "select * from bible where book=? and chapter=?" (list book-number book-chapter)))
;; (get-bible-data-book-chapter 46 5)
;; (get-bible-data-book-chapter 1 6)
(defun get-bible-data-book-chapter-verses (book-number book-chapter verse-min verse-max)
"Return scripture from BOOK-NUMBER, BOOK-CHAPTER, VERSE-MIN to VERSE-MAX."
(sqlite-execute sword-db
"select * from bible where book=? and chapter=? and verse>=? and verse<=?;"
(list book-number book-chapter verse-min verse-max)))
;; (get-bible-data-book-chapter-verses 1 6 1 4)
(defun split-input-data (str)
"Produces a list with (list book chapter verses) from input STR."
(let* ((full-string (s-split " " str))
(chapter-verse (car (last full-string)))
(book (s-join " " (butlast full-string)))
(chapter-verse-split (s-split ":" chapter-verse))
(verses? (length> chapter-verse-split 1))
(chapter (car chapter-verse-split))
(verses (s-split "-" (car (last chapter-verse-split)))))
(if verses?
(list book (string-to-number chapter) (cl-mapcar #'string-to-number verses))
(list book (string-to-number chapter) nil))))
;; (split-input-data "Genisis 6")
;; (split-input-data "Genisis 6:1")
;; (split-input-data "Genisis 6:1-2")
(defun get-bible-verses (str bible-sword-book-data)
"Interpret STR and return the verses. Use BIBLE-SWORD-BOOK-DATA."
(let* (
(location-data (split-input-data str))
(book-str (nth 0 location-data))
(book-num (get-book-number bible-sword-book-data book-str))
(chapter (nth 1 location-data))
(verses (nth 2 location-data))
(any-verses? (> (length verses) 0))
(multi-verses? (> (length verses) 1)))
;; (list any-verses? multi-verses?)
;; (list book-str book-num chapter verses any-verses? multi-verses?)
(cond
(multi-verses? ; if starting and ending verse
(get-bible-data-book-chapter-verses book-num chapter (car verses) (cadr verses)))
(any-verses? ; if just one verse
(get-bible-data-book-chapter-verses book-num chapter (car verses) (car verses)))
(t
(get-bible-data-book-chapter book-num chapter)))))
;; (get-bible-verses "Gen 3:1-2" sword-book-data)
;; (get-bible-verses "Gen 3:1" sword-book-data)
;; (get-bible-verses "Gen 3" sword-book-data)
;; TODO: Text has pop up notes like this? {\cf2\super [18]}
;; http://www.biblesupport.com/topic/11841-adding-pop-up-notes-to-e-sword-bible-modules/page-2
(defun start-of-verse-is-continuation (text)
"Check if TEXT begins with and/then."
(or
(s-starts-with? "and" text t)
(s-starts-with? "then" text t)
(s-starts-with? "that" text t)))
(defun end-of-verse-is-continuation (text)
"Check if TEXT ends with continuation punctuation."
(or
(s-ends-with? ";" text t)
(s-ends-with? ":" text t)
(s-ends-with? "," text t)))
;; (caddr '(4 19 1 "Now"))
;; (4 19 1 " Now the Lord spoke to Moses and Aaron, saying,")
(defun insert-scripture-in-buffer (data book-data)
"Add the scripture DATA to the current buffer using BOOK-DATA."
(if (not (or (null data) (equal data 0)))
;; If scripture data was returned, insert it into the buffer
(let* ((in-org-buffer (derived-mode-p 'org-mode))
(in-markdown-buffer (derived-mode-p 'markdown-mode))
(book-name (get-book-name-from-num sword-book-data (caar data)))
(book-chapter (cadar data))
(first-verse (car data))
(last-verse (car (last data)))
(first-verse-num (caddr first-verse))
(last-verse-num (caddr last-verse))
(one-verse? (equal first-verse last-verse))
(verse-info (s-concat
book-name " "
(number-to-string book-chapter) ":"
(if one-verse?
(number-to-string first-verse-num)
(s-concat (number-to-string first-verse-num) "-" (number-to-string last-verse-num)))
" "
sword-bblx-version-acroym)))
(cond
(in-org-buffer (print "In ORG buffer."))
(in-markdown-buffer (print "In Markdown buffer."))
(t (print "In text buffer.")))
;; Nicely inserts the book and verse numbers in the quote data.
;; Update: quotes are not formatted nicely if info is in +quote: data.
(when in-org-buffer
(insert
(s-concat
"\n#+verse: "
verse-info
"" ; End of 'verse info' line.
"\n#+begin_quote\n")))
(when in-markdown-buffer
(insert "> "))
(let ((insert-point (point)))
;; Insert lines of scripture
(dolist (line data)
(let (
(text (s-trim (nth 3 line)))
(first-verse? (equal line first-verse))
(last-verse? (equal line last-verse)))
;; If it's not the first line or a continuation, prepend a newline
(when (and
(not (start-of-verse-is-continuation text))
(not first-verse?))
(insert ?\n)
(when in-markdown-buffer (insert "> ")))
;; Add the scripture into the buffer
(insert text)
;; If it's not
(when (and
(not (end-of-verse-is-continuation text))
(not last-verse?))
(insert ?\n)
(when in-markdown-buffer (insert "> ")))))
;; For now, just remove the footnotes
(replace-regexp-in-region "{.*}" "" insert-point (point))
(fill-region insert-point (point)))
;; Close out quote block
(when in-org-buffer
(insert "\n#+end_quote\n"))
(when in-markdown-buffer
(insert (s-concat "\n> \n> -- " verse-info)))
(when (not (or in-markdown-buffer in-org-buffer))
(insert (s-concat "\n\n -- " verse-info))))
;; If no data is returned
(message "Couldn't find that verse.")))
(defun r/bible-insert-verse ()
"Insert the specified bible verse in org format into the current buffer."
(interactive)
;; First, grab the data from the sqlite database
(let* ((verse (read-string "Enter verse: "))
(data (get-bible-verses verse sword-book-data)))
(insert-scripture-in-buffer data sword-book-data)))
(defun r/bible-random-verse ()
"Insert the specified bible verse in org format into the current buffer."
(interactive)
;; First, grab the data from the sqlite database
(let* ((data (get-random-scripture sword-book-data)))
(insert-scripture-in-buffer data sword-book-data)))
(defun r/bible-random-proverb ()
"Insert the specified bible verse in org format into the current buffer."
(interactive)
;; First, grab the data from the sqlite database
(let* ((data (get-random-proverb sword-book-data)))
(insert-scripture-in-buffer data sword-book-data)))
(defun bible-insert ()
"Call r/insert-bible-verse."
(interactive)
(r/bible-insert-verse))
(provide 'rcf-sword)
;;; rcf-sword.el ends here
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment