summaryrefslogtreecommitdiff
path: root/modules/org-reveal-config.el
blob: 3ab80315f22ec98f62031b341dc8231197b1b1cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
;;; org-reveal-config.el --- Reveal.js Presentation Configuration -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>

;;; Commentary:
;; Integrates ox-reveal for creating reveal.js presentations from Org files.
;;
;; Fully offline workflow using a local reveal.js clone (managed by
;; scripts/setup-reveal.sh) and self-contained HTML export.
;;
;; Keybindings (C-; p prefix):
;; - C-; p e : Export to self-contained HTML and open in browser
;; - C-; p p : Start live preview (re-exports on save)
;; - C-; p s : Stop live preview
;; - C-; p h : Insert #+REVEAL_ header block at top of current buffer
;; - C-; p n : Create new presentation file (prompts for title and location)

;;; Code:

;; Forward declarations for byte-compiler (ox-reveal loaded via use-package)
(defvar org-reveal-root)
(defvar org-reveal-single-file)
(defvar org-reveal-plugins)
(defvar org-reveal-highlight-css)
(defvar org-reveal-init-options)
(declare-function org-reveal-export-to-html "ox-reveal")

;; --------------------------------- Constants ---------------------------------

(defconst cj/reveal-root
  (expand-file-name "reveal.js" user-emacs-directory)
  "Local reveal.js installation directory.")

(defconst cj/reveal-default-theme "black"
  "Default reveal.js theme for new presentations.")

(defconst cj/reveal-default-transition "slide"
  "Default reveal.js slide transition for new presentations.")

;; --------------------------------- ox-reveal ---------------------------------

(use-package ox-reveal
  :after ox
  :config
  (setq org-reveal-root (concat "file://" cj/reveal-root))
  (setq org-reveal-single-file t)
  (setq org-reveal-plugins '(highlight notes search zoom))
  (setq org-reveal-highlight-css "%r/plugin/highlight/monokai.css")
  (setq org-reveal-init-options "slideNumber:true, hash:true"))

;; ----------------------------- Private Helpers -------------------------------

(defun cj/--reveal-header-template (title)
  "Return the reveal.js header block string for TITLE."
  (unless (stringp title)
    (user-error "Title must be a string"))
  (format "#+TITLE: %s
#+AUTHOR: %s
#+DATE: %s
#+REVEAL_ROOT: %s
#+REVEAL_THEME: %s
#+REVEAL_TRANS: %s
#+REVEAL_INIT_OPTIONS: slideNumber:true, hash:true
#+REVEAL_PLUGINS: (highlight notes search zoom)
#+REVEAL_HIGHLIGHT_CSS: %%r/plugin/highlight/monokai.css
#+OPTIONS: toc:nil num:nil

" title (user-full-name) (format-time-string "%Y-%m-%d")
    (concat "file://" cj/reveal-root)
    cj/reveal-default-theme
    cj/reveal-default-transition))

(defun cj/--reveal-title-to-filename (title)
  "Convert TITLE to a slug-based .org filename.
Downcases TITLE, replaces whitespace runs with hyphens, appends .org."
  (concat (replace-regexp-in-string "[[:space:]]+" "-" (downcase title))
          ".org"))

(defun cj/--reveal-preview-export-on-save ()
  "Export current org buffer to reveal.js HTML silently.
Intended for use as a buffer-local `after-save-hook'."
  (when (derived-mode-p 'org-mode)
    (let ((inhibit-message t))
      (org-reveal-export-to-html))))

;; ----------------------------- Public Functions ------------------------------

(defun cj/reveal-export ()
  "Export current Org buffer to self-contained reveal.js HTML and open in browser."
  (interactive)
  (unless (derived-mode-p 'org-mode)
    (user-error "Not in an Org buffer"))
  (let ((html-file (org-reveal-export-to-html)))
    (when html-file
      (browse-url-of-file html-file)
      (message "Opened presentation: %s" html-file))))

(defun cj/reveal-preview-start ()
  "Start live preview: re-export to HTML on every save.
Opens the presentation in a browser on first call.  Subsequent saves
re-export silently; refresh the browser to see changes."
  (interactive)
  (unless (derived-mode-p 'org-mode)
    (user-error "Not in an Org buffer"))
  (add-hook 'after-save-hook #'cj/--reveal-preview-export-on-save nil t)
  (let ((html-file (org-reveal-export-to-html)))
    (when html-file
      (browse-url-of-file html-file)))
  (message "Live preview started — save to re-export, refresh browser to update"))

(defun cj/reveal-preview-stop ()
  "Stop live preview by removing the after-save-hook."
  (interactive)
  (remove-hook 'after-save-hook #'cj/--reveal-preview-export-on-save t)
  (message "Live preview stopped"))

(defun cj/reveal-insert-header ()
  "Insert a #+REVEAL_ header block at the top of the current Org buffer."
  (interactive)
  (unless (derived-mode-p 'org-mode)
    (user-error "Not in an Org buffer"))
  (let ((title (read-from-minibuffer "Presentation title: ")))
    (save-excursion
      (goto-char (point-min))
      (insert (cj/--reveal-header-template title)))
    (message "Inserted reveal.js headers")))

(defun cj/reveal-new ()
  "Create a new reveal.js presentation file.
Prompts for a title and save location, then opens the file with
reveal.js headers pre-filled."
  (interactive)
  (let* ((title (read-from-minibuffer "Presentation title: "))
         (default-dir (expand-file-name "~/"))
         (file (read-file-name "Save presentation to: " default-dir nil nil
                               (cj/--reveal-title-to-filename title))))
    (when (file-exists-p file)
      (user-error "File already exists: %s" file))
    (find-file file)
    (insert (cj/--reveal-header-template title))
    (insert "* Slide 1\n\n")
    (save-buffer)
    (message "New presentation: %s" file)))

;; -------------------------------- Keybindings --------------------------------

(global-set-key (kbd "C-; p e") #'cj/reveal-export)
(global-set-key (kbd "C-; p p") #'cj/reveal-preview-start)
(global-set-key (kbd "C-; p s") #'cj/reveal-preview-stop)
(global-set-key (kbd "C-; p h") #'cj/reveal-insert-header)
(global-set-key (kbd "C-; p n") #'cj/reveal-new)

(with-eval-after-load 'which-key
  (which-key-add-key-based-replacements
    "C-; p" "presentations"
    "C-; p e" "export & open"
    "C-; p p" "start live preview"
    "C-; p s" "stop live preview"
    "C-; p h" "insert headers"
    "C-; p n" "new presentation"))

(provide 'org-reveal-config)
;;; org-reveal-config.el ends here