aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-25 22:23:43 -0500
committerCraig Jennings <c@cjennings.net>2026-05-25 22:23:43 -0500
commit768c9c0122130d44e9243f22f7681677623b713a (patch)
tree81d234dde8117dafb9fd67b4b90d918108801c9d
parentc9ff295f02c355f889d1605853c148bbce404d52 (diff)
downloadpearl-768c9c0122130d44e9243f22f7681677623b713a.tar.gz
pearl-768c9c0122130d44e9243f22f7681677623b713a.zip
feat(render): add the pure helpers for workspace-derived TODO keywords
Two pure functions behind the coming workspace-faithful #+TODO line. pearl--state-name-to-keyword slugifies a Linear state name to an Org keyword: upcased, non-alphanumeric runs collapsed to a single hyphen, edges trimmed, Unicode letters kept (so "Ångström" survives), and an all-symbol name falls back to TODO. pearl--derive-todo-line turns an ordered state list into the "ACTIVE... | DONE..." string: completed/canceled/duplicate types go after the bar, the rest before, order preserved within each side, de-duplicated by slug, with the hardcoded default returned for an empty list. There's no caller yet -- the render integration and the gather pipeline land next. This is the derivation half of the keyword spec. The sync-back half it described is superseded by save-model-v2 (state reconciles at save) and the separate keyword-cycle task.
-rw-r--r--pearl.el35
-rw-r--r--tests/test-pearl-todo-keywords.el87
2 files changed, 122 insertions, 0 deletions
diff --git a/pearl.el b/pearl.el
index f148992..b6e1e8e 100644
--- a/pearl.el
+++ b/pearl.el
@@ -1534,6 +1534,41 @@ STATE is the Linear state string."
(or (cdr (assoc state pearl-state-to-todo-mapping))
"TODO")) ; Default fallback
+(defun pearl--state-name-to-keyword (name)
+ "Slugify a Linear state NAME to an Org TODO keyword.
+Upcased, each run of non-alphanumerics replaced by a single hyphen (alnum is
+Unicode-aware, so accented and non-Latin letters survive), with leading and
+trailing hyphens trimmed. An all-symbol name (empty slug) falls back to
+\"TODO\". This is the workspace-faithful replacement for the static
+`pearl-state-to-todo-mapping' lookup."
+ (let ((slug (string-trim
+ (replace-regexp-in-string "[^[:alnum:]]+" "-" (upcase (or name "")))
+ "-+" "-+")))
+ (if (string-empty-p slug) "TODO" slug)))
+
+(defun pearl--derive-todo-line (states)
+ "Build the `#+TODO:' keyword string from an ordered STATES list.
+Each state is a plist with at least :name and :type. States whose :type is
+completed / canceled / duplicate go after the `|' (the done side); the rest go
+before it. The caller's order is preserved within each side; keywords are
+slugified (`pearl--state-name-to-keyword') and de-duplicated by slug, first-seen
+winning. An empty STATES list returns the hardcoded default so the rendered
+file always has a valid `#+TODO:' line."
+ (if (null states)
+ "TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE"
+ (let ((active '()) (done '()) (seen (make-hash-table :test 'equal)))
+ (dolist (s states)
+ (let ((kw (pearl--state-name-to-keyword (plist-get s :name))))
+ (unless (gethash kw seen)
+ (puthash kw t seen)
+ (if (member (plist-get s :type) '("completed" "canceled" "duplicate"))
+ (push kw done)
+ (push kw active)))))
+ (let ((active-str (string-join (nreverse active) " "))
+ (done-str (string-join (nreverse done) " ")))
+ (concat active-str " |"
+ (if (string-empty-p done-str) "" (concat " " done-str)))))))
+
(defun pearl--get-todo-states-pattern ()
"Return the regex pattern matching the Org TODO states.
Built from the org keywords in `pearl-state-to-todo-mapping' and
diff --git a/tests/test-pearl-todo-keywords.el b/tests/test-pearl-todo-keywords.el
new file mode 100644
index 0000000..a167157
--- /dev/null
+++ b/tests/test-pearl-todo-keywords.el
@@ -0,0 +1,87 @@
+;;; test-pearl-todo-keywords.el --- Tests for deriving TODO keywords from states -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for the pure derivation helpers behind the workspace-derived #+TODO
+;; line: `pearl--state-name-to-keyword' (slugify a Linear state name to an Org
+;; keyword) and `pearl--derive-todo-line' (an ordered state list to the
+;; "ACTIVE... | DONE..." keyword string).
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+
+;;; --state-name-to-keyword (slugify)
+
+(ert-deftest test-pearl-state-name-to-keyword-spaces-and-case ()
+ "Spaces become hyphens and the name is upcased."
+ (should (string= "DEV-REVIEW" (pearl--state-name-to-keyword "Dev Review")))
+ (should (string= "IN-PROGRESS" (pearl--state-name-to-keyword "In Progress")))
+ (should (string= "TODO" (pearl--state-name-to-keyword "Todo"))))
+
+(ert-deftest test-pearl-state-name-to-keyword-punctuation-runs ()
+ "A run of non-alphanumerics collapses to a single hyphen; edges trimmed."
+ (should (string= "PM-ACCEPTANCE" (pearl--state-name-to-keyword "PM Acceptance")))
+ (should (string= "BACKLOG-PRIORITIZED"
+ (pearl--state-name-to-keyword "Backlog (prioritized)"))))
+
+(ert-deftest test-pearl-state-name-to-keyword-unicode-preserved ()
+ "Unicode letters are kept (alnum is Unicode-aware), not stripped."
+ (should (string= "ÅNGSTRÖM" (pearl--state-name-to-keyword "Ångström"))))
+
+(ert-deftest test-pearl-state-name-to-keyword-empty-slug-falls-back ()
+ "An all-symbol name (empty slug) falls back to TODO, as does the empty string."
+ (should (string= "TODO" (pearl--state-name-to-keyword "!!!")))
+ (should (string= "TODO" (pearl--state-name-to-keyword ""))))
+
+;;; --derive-todo-line
+
+(ert-deftest test-pearl-derive-todo-line-partitions-by-type ()
+ "Completed/canceled/duplicate states go after the bar; the rest before it."
+ (should (string= "TODO IN-PROGRESS | DONE CANCELED"
+ (pearl--derive-todo-line
+ '((:name "Todo" :type "unstarted")
+ (:name "In Progress" :type "started")
+ (:name "Done" :type "completed")
+ (:name "Canceled" :type "canceled"))))))
+
+(ert-deftest test-pearl-derive-todo-line-dedups-by-slug ()
+ "Two names slugifying to the same keyword list it once (first-seen)."
+ (should (string= "DEV-REVIEW |"
+ (pearl--derive-todo-line
+ '((:name "Dev Review" :type "started")
+ (:name "Dev-Review" :type "started"))))))
+
+(ert-deftest test-pearl-derive-todo-line-preserves-input-order ()
+ "Within each side, the caller's order is preserved."
+ (should (string= "BACKLOG TODO IN-PROGRESS | DONE"
+ (pearl--derive-todo-line
+ '((:name "Backlog" :type "backlog")
+ (:name "Todo" :type "unstarted")
+ (:name "In Progress" :type "started")
+ (:name "Done" :type "completed"))))))
+
+(ert-deftest test-pearl-derive-todo-line-empty-is-hardcoded-fallback ()
+ "An empty state list yields the hardcoded default so the file stays valid."
+ (should (string= "TODO IN-PROGRESS IN-REVIEW BACKLOG BLOCKED | DONE"
+ (pearl--derive-todo-line '()))))
+
+(provide 'test-pearl-todo-keywords)
+;;; test-pearl-todo-keywords.el ends here