summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/hugo-config.el135
-rw-r--r--modules/org-export-config.el56
-rw-r--r--modules/org-reveal-config.el162
-rw-r--r--modules/user-constants.el3
4 files changed, 305 insertions, 51 deletions
diff --git a/modules/hugo-config.el b/modules/hugo-config.el
new file mode 100644
index 00000000..33245fd4
--- /dev/null
+++ b/modules/hugo-config.el
@@ -0,0 +1,135 @@
+;;; hugo-config.el --- Hugo Blog Configuration -*- lexical-binding: t; coding: utf-8; -*-
+;; author: Craig Jennings <c@cjennings.net>
+
+;;; Commentary:
+;; Integrates ox-hugo for publishing Org files to a Hugo website.
+;;
+;; One-file-per-post workflow:
+;; - Each blog post is a standalone Org file in content-org/log/
+;; - File-level keywords control Hugo front matter
+;; - Export with C-; h e, create new posts with C-; h n
+;;
+;; Keybindings (C-; h prefix):
+;; - C-; h n : New post (create from template)
+;; - C-; h e : Export current post to Hugo markdown
+;; - C-; h o : Open blog source directory in dirvish
+;; - C-; h O : Open blog source directory in system file manager
+;; - C-; h d : Toggle draft status (TODO/DONE)
+
+;;; Code:
+
+(require 'user-constants)
+(require 'host-environment)
+
+;; --------------------------------- Constants ---------------------------------
+
+(defconst cj/hugo-content-org-dir
+ (expand-file-name "content-org/log/" website-dir)
+ "Directory containing Org source files for Hugo blog posts.")
+
+;; ---------------------------------- ox-hugo ----------------------------------
+
+(use-package ox-hugo
+ :after ox)
+
+;; ----------------------------- Hugo Blog Functions ---------------------------
+
+(defun cj/hugo--post-file-path (title)
+ "Return the file path for a Hugo post with TITLE.
+Generates a slug from TITLE using `org-hugo-slug' and returns
+the full path under `cj/hugo-content-org-dir'.
+Assumes ox-hugo is already loaded (via use-package declaration above)."
+ (let ((slug (org-hugo-slug title)))
+ (expand-file-name (concat slug ".org") cj/hugo-content-org-dir)))
+
+(defun cj/hugo--post-template (title date)
+ "Return the Org front matter template for a Hugo post.
+TITLE is the post title, DATE is the date string (YYYY-MM-DD)."
+ (format "#+hugo_base_dir: ../../
+#+hugo_section: log
+#+hugo_auto_set_lastmod: t
+#+title: %s
+#+date: %s
+#+hugo_tags:
+#+hugo_draft: true
+#+hugo_custom_front_matter: :description \"\"
+
+" title date))
+
+(defun cj/hugo-new-post ()
+ "Create a new Hugo blog post as a standalone Org file.
+Prompts for title, generates the slug filename, and opens the
+new file with Hugo front matter keywords pre-filled."
+ (interactive)
+ (let* ((title (read-from-minibuffer "Post Title: "))
+ (file (cj/hugo--post-file-path title))
+ (date (format-time-string "%Y-%m-%d")))
+ (when (file-exists-p file)
+ (user-error "Post already exists: %s" file))
+ (unless (file-directory-p cj/hugo-content-org-dir)
+ (make-directory cj/hugo-content-org-dir t))
+ (find-file file)
+ (insert (cj/hugo--post-template title date))
+ (save-buffer)
+ (message "New post: %s" file)))
+
+(defun cj/hugo-export-post ()
+ "Export the current Org file to Hugo-compatible Markdown."
+ (interactive)
+ (require 'ox-hugo)
+ (unless (derived-mode-p 'org-mode)
+ (user-error "Not in an Org buffer"))
+ (org-hugo-export-to-md)
+ (message "Exported: %s" (buffer-name)))
+
+(defun cj/hugo-open-blog-dir ()
+ "Open the blog source directory in dirvish/dired."
+ (interactive)
+ (unless (file-directory-p cj/hugo-content-org-dir)
+ (make-directory cj/hugo-content-org-dir t))
+ (dired cj/hugo-content-org-dir))
+
+(defun cj/hugo-open-blog-dir-external ()
+ "Open the blog source directory in the system file manager."
+ (interactive)
+ (unless (file-directory-p cj/hugo-content-org-dir)
+ (make-directory cj/hugo-content-org-dir t))
+ (let ((cmd (cond
+ ((env-macos-p) "open")
+ ((env-windows-p) "explorer.exe")
+ (t "xdg-open"))))
+ (start-process "hugo-file-manager" nil cmd cj/hugo-content-org-dir)))
+
+(defun cj/hugo-toggle-draft ()
+ "Toggle the draft status of the current Hugo post.
+Switches #+hugo_draft between true and false."
+ (interactive)
+ (save-excursion
+ (goto-char (point-min))
+ (if (re-search-forward "^#\\+hugo_draft: *\\(true\\|false\\)" nil t)
+ (let ((current (match-string 1)))
+ (replace-match (if (string= current "true") "false" "true") t t nil 1)
+ (save-buffer)
+ (message "Draft: %s → %s" current
+ (if (string= current "true") "false" "true")))
+ (user-error "No #+hugo_draft keyword found in this file"))))
+
+;; -------------------------------- Keybindings --------------------------------
+
+(global-set-key (kbd "C-; h n") #'cj/hugo-new-post)
+(global-set-key (kbd "C-; h e") #'cj/hugo-export-post)
+(global-set-key (kbd "C-; h o") #'cj/hugo-open-blog-dir)
+(global-set-key (kbd "C-; h O") #'cj/hugo-open-blog-dir-external)
+(global-set-key (kbd "C-; h d") #'cj/hugo-toggle-draft)
+
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; h" "hugo blog menu"
+ "C-; h n" "new post"
+ "C-; h e" "export post"
+ "C-; h o" "open in dirvish"
+ "C-; h O" "open in file manager"
+ "C-; h d" "toggle draft"))
+
+(provide 'hugo-config)
+;;; hugo-config.el ends here
diff --git a/modules/org-export-config.el b/modules/org-export-config.el
index 612b80cb..4451eddd 100644
--- a/modules/org-export-config.el
+++ b/modules/org-export-config.el
@@ -13,14 +13,14 @@
;; - Texinfo: GNU documentation and Info files
;;
;; Extended via Pandoc:
-;; - Additional formats: DOCX, EPUB, reveal.js presentations
-;; - Self-contained HTML exports with embedded resources
+;; - Additional formats: DOCX, self-contained HTML5
;; - Custom PDF export with Zathura integration
;;
;; Key features:
;; - UTF-8 encoding enforced across all backends
;; - Subtree export as default scope
-;; - reveal.js presentations with CDN or local embedding options
+;;
+;; Note: reveal.js presentations are handled by org-reveal-config.el (C-; p)
;;
;;; Code:
@@ -79,47 +79,7 @@
(setq org-pandoc-options '((standalone . t)
(mathjax . t)))
- ;; Configure reveal.js specific options
- (setq org-pandoc-options-for-revealjs
- '((standalone . t)
- (variable . "revealjs-url=https://cdn.jsdelivr.net/npm/reveal.js")
- (variable . "theme=black") ; or white, league, beige, sky, night, serif, simple, solarized
- (variable . "transition=slide") ; none, fade, slide, convex, concave, zoom
- (variable . "slideNumber=true")
- (variable . "hash=true")
- (self-contained . t))) ; This embeds CSS/JS when possible
-
- ;; Custom function for self-contained reveal.js export
- (defun my/org-pandoc-export-to-revealjs-standalone ()
- "Export to reveal.js with embedded dependencies."
- (interactive)
- (let* ((org-pandoc-options-for-revealjs
- (append org-pandoc-options-for-revealjs
- '((self-contained . t)
- (embed-resources . t)))) ; pandoc 3.0+ option
- (html-file (org-pandoc-export-to-revealjs)))
- (when html-file
- (browse-url-of-file html-file)
- (message "Opened reveal.js presentation: %s" html-file))))
-
- ;; Alternative: Download and embed local reveal.js
- (defun my/org-pandoc-export-to-revealjs-local ()
- "Export to reveal.js using local copy of reveal.js."
- (interactive)
- (let* ((reveal-dir (expand-file-name "reveal.js" user-emacs-directory))
- (org-pandoc-options-for-revealjs
- `((standalone . t)
- (variable . ,(format "revealjs-url=%s" reveal-dir))
- (variable . "theme=black")
- (variable . "transition=slide")
- (variable . "slideNumber=true"))))
- (unless (file-exists-p reveal-dir)
- (cj/log-silently "Downloading reveal.js...")
- (shell-command
- (format "git clone https://github.com/hakimel/reveal.js.git %s" reveal-dir)))
- (org-pandoc-export-to-revealjs)))
-
- ;; Configure specific format options (your existing config)
+ ;; Configure specific format options
(setq org-pandoc-options-for-latex-pdf '((pdf-engine . "pdflatex")))
(setq org-pandoc-options-for-html5 '((html-q-tags . t)
(self-contained . t)))
@@ -134,20 +94,14 @@
(start-process "zathura-pdf" nil "zathura" pdf-file)
(message "Opened %s in Zathura" pdf-file))))
- ;; Updated menu entries with reveal.js options
+ ;; Pandoc export menu entries
(setq org-pandoc-menu-entry
'((?4 "to html5 and open" org-pandoc-export-to-html5-and-open)
(?$ "as html5" org-pandoc-export-as-html5)
- (?r "to reveal.js (CDN) and open" org-pandoc-export-to-revealjs-and-open)
- (?R "to reveal.js (self-contained)" my/org-pandoc-export-to-revealjs-standalone)
(?< "to markdown" org-pandoc-export-to-markdown)
(?d "to docx and open" org-pandoc-export-to-docx-and-open)
(?z "to pdf and open (Zathura)" my/org-pandoc-export-to-pdf-and-open))))
-;; hugo markdown
-;; (use-package ox-hugo
-;; :after ox)
-
;; github flavored markdown
;; (use-package ox-gfm
;; :after ox)
diff --git a/modules/org-reveal-config.el b/modules/org-reveal-config.el
new file mode 100644
index 00000000..3ab80315
--- /dev/null
+++ b/modules/org-reveal-config.el
@@ -0,0 +1,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
diff --git a/modules/user-constants.el b/modules/user-constants.el
index 85890f97..bae34bfe 100644
--- a/modules/user-constants.el
+++ b/modules/user-constants.el
@@ -119,6 +119,9 @@ Used by transcription module and other audio-related functionality.")
(defconst music-dir (expand-file-name "music/" user-home-dir)
"The location to save your music files.")
+(defconst website-dir (expand-file-name "projects/website/" user-home-dir)
+ "Root directory of the Hugo website project.")
+
;; FILES
(defvar authinfo-file (expand-file-name ".authinfo.gpg" user-home-dir)