diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/hugo-config.el | 135 | ||||
| -rw-r--r-- | modules/org-export-config.el | 56 | ||||
| -rw-r--r-- | modules/org-reveal-config.el | 162 | ||||
| -rw-r--r-- | modules/user-constants.el | 3 |
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) |
