From 176ea668cdd83beddd54a24334a8a9db3cc87dfb Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 14 Feb 2026 03:21:13 -0600 Subject: feat(reveal): add org-reveal presentation workflow with ERT tests Replaced pandoc-based reveal.js export with native ox-reveal integration. New org-reveal-config.el module provides offline, self-contained HTML export with keybindings under C-; p. Includes setup script for reveal.js 5.1.0 and 34 ERT tests covering header template and title-to-filename helpers. --- .gitignore | 3 + init.el | 1 + modules/org-export-config.el | 52 +------ modules/org-reveal-config.el | 162 ++++++++++++++++++++++ scripts/setup-reveal.sh | 71 ++++++++++ tests/test-org-reveal-config-header-template.el | 144 +++++++++++++++++++ tests/test-org-reveal-config-title-to-filename.el | 109 +++++++++++++++ todo.org | 19 ++- 8 files changed, 512 insertions(+), 49 deletions(-) create mode 100644 modules/org-reveal-config.el create mode 100755 scripts/setup-reveal.sh create mode 100644 tests/test-org-reveal-config-header-template.el create mode 100644 tests/test-org-reveal-config-title-to-filename.el diff --git a/.gitignore b/.gitignore index 691845df..c7adf7af 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ auto-save-list/ /browser-choice.el /client_secret_491339091045-sjje1r54s22vn2ugh45khndjafp89vto.apps.googleusercontent.com.json +# reveal.js local clone (managed by scripts/setup-reveal.sh) +/reveal.js/ + # Documentation folder (session notes, personal workflows) /docs/ diff --git a/init.el b/init.el index 4926ae1a..c98f49fb 100644 --- a/init.el +++ b/init.el @@ -115,6 +115,7 @@ (require 'org-drill-config) (require 'org-export-config) (require 'hugo-config) ;; ox-hugo blog workflow (C-; h) +(require 'org-reveal-config) ;; reveal.js presentations (C-; p) (require 'org-refile-config) ;; refile org-branches (require 'org-roam-config) ;; personal knowledge management in org mode diff --git a/modules/org-export-config.el b/modules/org-export-config.el index 33e170f5..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,12 +94,10 @@ (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)))) 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 + +;;; 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/scripts/setup-reveal.sh b/scripts/setup-reveal.sh new file mode 100755 index 00000000..c28e0a6c --- /dev/null +++ b/scripts/setup-reveal.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Setup reveal.js for org-reveal presentations (offline, pinned version) +# Usage: setup-reveal.sh [--yes] # --yes for non-interactive mode + +set -euo pipefail + +REVEAL_VERSION="5.1.0" +REVEAL_DIR="$HOME/.emacs.d/reveal.js" + +# Non-interactive mode +ASSUME_YES=false +if [[ "${1:-}" == "--yes" ]] || [[ "${1:-}" == "-y" ]]; then + ASSUME_YES=true +fi + +echo "=== reveal.js Setup for org-reveal ===" +echo + +# Check if correct version already installed +if [[ -d "$REVEAL_DIR" ]]; then + if [[ -f "$REVEAL_DIR/dist/reveal.js" ]]; then + INSTALLED_VERSION=$(cd "$REVEAL_DIR" && git describe --tags 2>/dev/null || echo "unknown") + if [[ "$INSTALLED_VERSION" == "$REVEAL_VERSION" ]]; then + echo "✓ reveal.js $REVEAL_VERSION already installed at $REVEAL_DIR" + exit 0 + else + echo "Found reveal.js $INSTALLED_VERSION, need $REVEAL_VERSION" + if [[ "$ASSUME_YES" == false ]]; then + read -p "Replace with correct version? [Y/n] " -n 1 -r + echo + if [[ $REPLY =~ ^[Nn]$ ]]; then + echo "Aborted." + exit 1 + fi + fi + echo "Removing old version..." + rm -rf "$REVEAL_DIR" + fi + else + echo "Found incomplete reveal.js directory, removing..." + rm -rf "$REVEAL_DIR" + fi +fi + +# Clone reveal.js at pinned version (shallow clone for speed) +echo "Step 1/2: Cloning reveal.js $REVEAL_VERSION..." +git clone --depth 1 --branch "$REVEAL_VERSION" \ + https://github.com/hakimel/reveal.js.git "$REVEAL_DIR" +echo "✓ Cloned reveal.js $REVEAL_VERSION" + +# Verify installation +echo +echo "Step 2/2: Verifying installation..." +if [[ -f "$REVEAL_DIR/dist/reveal.js" ]]; then + echo "✓ reveal.js $REVEAL_VERSION installed at $REVEAL_DIR" + echo + echo "=== Setup Complete! ===" + echo + echo "Usage in Emacs:" + echo " C-; p n Create new presentation" + echo " C-; p e Export to HTML and open" + echo " C-; p p Start live preview" +else + echo "✗ Installation failed - dist/reveal.js not found" + echo + echo "Troubleshooting:" + echo "1. Check git access to github.com" + echo "2. Verify disk space at $REVEAL_DIR" + echo "3. Try manual clone: git clone https://github.com/hakimel/reveal.js.git $REVEAL_DIR" + exit 1 +fi diff --git a/tests/test-org-reveal-config-header-template.el b/tests/test-org-reveal-config-header-template.el new file mode 100644 index 00000000..11939c55 --- /dev/null +++ b/tests/test-org-reveal-config-header-template.el @@ -0,0 +1,144 @@ +;;; test-org-reveal-config-header-template.el --- Tests for cj/--reveal-header-template -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/--reveal-header-template function from org-reveal-config.el +;; +;; This function takes a title string and returns a complete #+REVEAL_ header +;; block for an org-reveal presentation. It uses user-full-name and +;; format-time-string internally, so we mock those for deterministic output. +;; The reveal.js constants (root, theme, transition) are tested via their +;; presence in the output. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub ox-reveal dependency (not available in batch mode) +(provide 'ox-reveal) + +(require 'org-reveal-config) + +;; Helper to call template with deterministic date and author +(defun test-reveal--header (title) + "Call cj/--reveal-header-template with TITLE, mocking time and user." + (cl-letf (((symbol-function 'user-full-name) (lambda () "Test Author")) + ((symbol-function 'format-time-string) + (lambda (_fmt) "2026-02-14"))) + (cj/--reveal-header-template title))) + +;;; Normal Cases + +(ert-deftest test-org-reveal-config-header-template-normal-contains-title () + "Output should contain #+TITLE: with the given title." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+TITLE: My Talk$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-author () + "Output should contain #+AUTHOR: with the user's full name." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+AUTHOR: Test Author$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-date () + "Output should contain #+DATE: with today's date." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+DATE: 2026-02-14$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-reveal-root () + "Output should contain #+REVEAL_ROOT: with file:// URL." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_ROOT: file://" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-theme () + "Output should contain #+REVEAL_THEME: with the default theme." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p + (format "^#\\+REVEAL_THEME: %s$" cj/reveal-default-theme) + result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-transition () + "Output should contain #+REVEAL_TRANS: with the default transition." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p + (format "^#\\+REVEAL_TRANS: %s$" cj/reveal-default-transition) + result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-init-options () + "Output should contain #+REVEAL_INIT_OPTIONS: with slideNumber and hash." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_INIT_OPTIONS:.*slideNumber" result)) + (should (string-match-p "^#\\+REVEAL_INIT_OPTIONS:.*hash" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-plugins () + "Output should contain #+REVEAL_PLUGINS: listing all plugins." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*highlight" result)) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*notes" result)) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*search" result)) + (should (string-match-p "^#\\+REVEAL_PLUGINS:.*zoom" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-highlight-css () + "Output should contain #+REVEAL_HIGHLIGHT_CSS: with monokai." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+REVEAL_HIGHLIGHT_CSS:.*monokai" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-contains-options () + "Output should contain #+OPTIONS: disabling toc and num." + (let ((result (test-reveal--header "My Talk"))) + (should (string-match-p "^#\\+OPTIONS: toc:nil num:nil$" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-ends-with-blank-line () + "Output should end with a trailing newline (blank line separator)." + (let ((result (test-reveal--header "My Talk"))) + (should (string-suffix-p "\n\n" result)))) + +(ert-deftest test-org-reveal-config-header-template-normal-all-keywords-present () + "All required org keywords should be present in the output." + (let ((result (test-reveal--header "My Talk")) + (keywords '("#+TITLE:" "#+AUTHOR:" "#+DATE:" + "#+REVEAL_ROOT:" "#+REVEAL_THEME:" "#+REVEAL_TRANS:" + "#+REVEAL_INIT_OPTIONS:" "#+REVEAL_PLUGINS:" + "#+REVEAL_HIGHLIGHT_CSS:" "#+OPTIONS:"))) + (dolist (kw keywords) + (should (string-match-p (regexp-quote kw) result))))) + +;;; Boundary Cases + +(ert-deftest test-org-reveal-config-header-template-boundary-empty-title () + "Empty title should produce valid header with empty #+TITLE." + (let ((result (test-reveal--header ""))) + (should (string-match-p "^#\\+TITLE: $" result)) + (should (string-match-p "#\\+REVEAL_THEME:" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-unicode-title () + "Unicode title should be preserved in #+TITLE." + (let ((result (test-reveal--header "日本語プレゼン"))) + (should (string-match-p "^#\\+TITLE: 日本語プレゼン$" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-title-with-special-chars () + "Special characters in title should not break the template." + (let ((result (test-reveal--header "What's New? (2026 Edition)"))) + (should (string-match-p "^#\\+TITLE: What's New\\? (2026 Edition)$" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-title-with-percent () + "Percent signs in title should not break format string." + (let ((result (test-reveal--header "100% Complete"))) + (should (string-match-p "^#\\+TITLE: 100% Complete$" result)))) + +(ert-deftest test-org-reveal-config-header-template-boundary-very-long-title () + "Very long title should produce valid output." + (let* ((long-title (make-string 200 ?x)) + (result (test-reveal--header long-title))) + (should (string-match-p "#\\+TITLE:" result)) + (should (string-match-p "#\\+REVEAL_THEME:" result)))) + +;;; Error Cases + +(ert-deftest test-org-reveal-config-header-template-error-nil-title () + "Nil title should signal an error rather than silently producing garbage." + (should-error (test-reveal--header nil) :type 'user-error)) + +(provide 'test-org-reveal-config-header-template) +;;; test-org-reveal-config-header-template.el ends here diff --git a/tests/test-org-reveal-config-title-to-filename.el b/tests/test-org-reveal-config-title-to-filename.el new file mode 100644 index 00000000..46296e68 --- /dev/null +++ b/tests/test-org-reveal-config-title-to-filename.el @@ -0,0 +1,109 @@ +;;; test-org-reveal-config-title-to-filename.el --- Tests for cj/--reveal-title-to-filename -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/--reveal-title-to-filename function from org-reveal-config.el +;; +;; This function takes a presentation title string, downcases it, replaces +;; whitespace runs with hyphens, and appends ".org". It is a pure string +;; function with no external dependencies — zero mocking required. + +;;; Code: + +(require 'ert) + +;; Add modules directory to load path +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; Stub ox-reveal dependency (not available in batch mode) +(provide 'ox-reveal) + +(require 'org-reveal-config) + +;;; Normal Cases + +(ert-deftest test-org-reveal-config-title-to-filename-normal-simple-title () + "Simple title should become lowercase-hyphenated.org." + (should (equal "my-first-talk.org" + (cj/--reveal-title-to-filename "My First Talk")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-two-words () + "Two-word title should produce single hyphen." + (should (equal "hello-world.org" + (cj/--reveal-title-to-filename "Hello World")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-already-lowercase () + "Already lowercase title should be unchanged except for extension." + (should (equal "demo-talk.org" + (cj/--reveal-title-to-filename "demo talk")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-single-word () + "Single word title should just get .org appended." + (should (equal "overview.org" + (cj/--reveal-title-to-filename "Overview")))) + +(ert-deftest test-org-reveal-config-title-to-filename-normal-always-org-extension () + "Result should always end with .org." + (should (string-suffix-p ".org" + (cj/--reveal-title-to-filename "Anything")))) + +;;; Boundary Cases + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-multiple-spaces () + "Multiple consecutive spaces should collapse to single hyphen." + (should (equal "foo-bar.org" + (cj/--reveal-title-to-filename "foo bar")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-tabs () + "Tabs should be treated as whitespace and replaced." + (should (equal "tab-separated.org" + (cj/--reveal-title-to-filename "tab\tseparated")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-mixed-whitespace () + "Mixed spaces and tabs should collapse to single hyphen." + (should (equal "mixed-ws.org" + (cj/--reveal-title-to-filename "mixed \t ws")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-leading-trailing-spaces () + "Leading and trailing spaces become leading/trailing hyphens." + (let ((result (cj/--reveal-title-to-filename " padded "))) + (should (equal "-padded-.org" result)))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-unicode-title () + "Unicode characters should be preserved (only whitespace replaced)." + (should (equal "日本語-talk.org" + (cj/--reveal-title-to-filename "日本語 Talk")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-numbers-in-title () + "Numbers should be preserved in slug." + (should (equal "q4-2026-results.org" + (cj/--reveal-title-to-filename "Q4 2026 Results")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-special-chars-preserved () + "Non-whitespace special characters should be preserved (not stripped)." + (should (equal "what's-new?.org" + (cj/--reveal-title-to-filename "What's New?")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-very-long-title () + "Very long title should still produce valid filename." + (let* ((long-title (mapconcat #'identity (make-list 20 "word") " ")) + (result (cj/--reveal-title-to-filename long-title))) + (should (string-suffix-p ".org" result)) + (should-not (string-match-p " " result)))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-empty-string () + "Empty string should produce just .org." + (should (equal ".org" (cj/--reveal-title-to-filename "")))) + +(ert-deftest test-org-reveal-config-title-to-filename-boundary-newline () + "Newlines should be treated as whitespace." + (should (equal "line-one-line-two.org" + (cj/--reveal-title-to-filename "Line One\nLine Two")))) + +;;; Error Cases + +(ert-deftest test-org-reveal-config-title-to-filename-error-nil-input () + "Nil input should signal an error (not crash silently)." + (should-error (cj/--reveal-title-to-filename nil))) + +(provide 'test-org-reveal-config-title-to-filename) +;;; test-org-reveal-config-title-to-filename.el ends here diff --git a/todo.org b/todo.org index b3ad44a3..27b5e366 100644 --- a/todo.org +++ b/todo.org @@ -1458,9 +1458,24 @@ CLOSED: [2025-11-12 Wed 02:41] Complete code already exists in someday-maybe.org. Need today and recurring. -** TODO [#A] Implement org-reveal presentation workflow +** DONE [#A] Implement org-reveal presentation workflow +CLOSED: [2026-02-14 Fri] -Create reveal.js slides from org-mode. +Created org-reveal-config.el module with ox-reveal integration for offline, +self-contained reveal.js presentations. Keybindings under C-; p prefix. +Removed pandoc-based reveal.js export from org-export-config.el. + +** TODO [#B] Create custom dupre reveal.js theme + +Create a custom reveal.js CSS theme using colors from themes/dupre-palette.el. +Install into reveal.js/css/theme/ for use with #+REVEAL_THEME: dupre. + +** TODO [#B] Investigate missing yasnippet configuration + +snippets-dir is defined in user-constants.el (points to org-dir/snippets/) and +yasnippet is configured in prog-general.el, but no custom snippets directory or +snippet files exist. Investigate whether snippets should be created (e.g., org +structure templates for reveal.js headers, blog post front matter, etc.). * Method 6: Develop Disciplined Engineering Practices [1/3] ** TODO [#C] Track current metrics baseline -- cgit v1.2.3