From 768c9c0122130d44e9243f22f7681677623b713a Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 25 May 2026 22:23:43 -0500 Subject: feat(render): add the pure helpers for workspace-derived TODO keywords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pearl.el | 35 ++++++++++++++++ tests/test-pearl-todo-keywords.el | 87 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 tests/test-pearl-todo-keywords.el 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 + +;; 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 . + +;;; 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 -- cgit v1.2.3