aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.org13
-rw-r--r--pearl.el23
-rw-r--r--tests/test-pearl-format.el6
-rw-r--r--tests/test-pearl-heading.el14
-rw-r--r--tests/test-pearl-output.el7
-rw-r--r--tests/test-pearl-save.el4
6 files changed, 49 insertions, 18 deletions
diff --git a/README.org b/README.org
index ad6c5a6..6dd3b92 100644
--- a/README.org
+++ b/README.org
@@ -266,6 +266,18 @@ A fetched Pearl file is intentionally readable. The header records the source, r
The =LINEAR-*= properties store both ids and display names so common commands do not need a network lookup just to render. The hash properties are provenance for conflict-aware sync.
+*** Fidelity to Linear
+:PROPERTIES:
+:CUSTOM_ID: fidelity-to-linear
+:END:
+
+Anything on the page that is Linear's data renders verbatim — the buffer shows the same text you would see opening the issue in Linear itself. Titles, the view name, assignee and label names, comment bodies: Pearl does not re-case, reword, or tidy them. There are exactly two transformations, and both are forced by Org's own syntax rather than chosen:
+
+- *State names become TODO keywords.* Org keywords cannot contain spaces, so "Dev Review" renders as the keyword =DEV-REVIEW=. The real state name is preserved verbatim in the =:LINEAR-STATE-NAME:= drawer property.
+- *Descriptions and comments convert between Markdown and Org.* Linear stores them as Markdown; the buffer renders them as Org and converts back on save. This is a representation translation, not an edit of the content, and round-trips for the supported subset.
+
+If you prefer a tidier outline, =pearl-title-case-headings= opts issue titles into smart title case (off by default, so titles match Linear out of the box).
+
** Configuration
:PROPERTIES:
:CUSTOM_ID: configuration
@@ -282,6 +294,7 @@ Most users only need an API key and an output path. The rest are knobs for teams
| =pearl-max-issue-pages= | Pagination cap, 100 issues per page |
| =pearl-request-timeout= | Synchronous request timeout in seconds |
| =pearl-fold-after-update= | Re-fold the active page after fetch/refresh |
+| =pearl-title-case-headings= | Opt-in smart title case for issue titles (off) |
| =pearl-surface-buffer= | Show the active buffer after a command updates it |
| =pearl-surface-select-window= | Move focus to the surfaced buffer |
| =pearl-debug= | Log request/response details to =*Messages*= |
diff --git a/pearl.el b/pearl.el
index 5dd55a7..c1492fc 100644
--- a/pearl.el
+++ b/pearl.el
@@ -169,15 +169,17 @@ leave the buffer expanded after updates."
:type 'boolean
:group 'pearl)
-(defcustom pearl-title-case-headings t
+(defcustom pearl-title-case-headings nil
"When non-nil, render issue titles in the heading in smart title case.
-Linear stores titles however they were typed (often sentence case); this
-capitalizes each word for a tidier outline, keeping minor words (articles,
-short conjunctions and prepositions) lowercase unless they are first or last,
-and leaving words that already contain an uppercase letter (acronyms like API,
-identifiers, camelCase) untouched. Display-only: the title's provenance hash
-is taken over the rendered form, so an unedited title is still a no-op on sync
-and is never rewritten on Linear. Set to nil to render titles verbatim."
+The default is nil: titles render verbatim, exactly as Linear stores them, so
+the buffer mirrors what you would see opening the issue in Linear itself.
+Set this to t for opt-in tidying — it capitalizes each word for a neater
+outline, keeping minor words (articles, short conjunctions and prepositions)
+lowercase unless they are first or last, and leaving words that already
+contain an uppercase letter (acronyms like API, identifiers, camelCase)
+untouched. Display-only either way: the title's provenance hash is taken over
+the rendered form, so an unedited title is still a no-op on sync and is never
+rewritten on Linear."
:type 'boolean
:group 'pearl)
@@ -2844,8 +2846,9 @@ yields the hardcoded default. Pure function, no side effects."
(name (pearl--source-name src))
(filter (plist-get src :filter)))
(with-temp-buffer
- (insert (format "#+title: Linear — %s\n"
- (if pearl-title-case-headings (pearl--title-case name) name)))
+ ;; The view name renders verbatim, matching Linear's own capitalization;
+ ;; `pearl-title-case-headings' governs issue headings only.
+ (insert (format "#+title: Linear — %s\n" name))
(insert "#+STARTUP: show3levels\n")
(insert (format "#+TODO: %s\n" (pearl--derive-todo-line states)))
;; Source-tracking metadata: the serialized source drives refresh; the
diff --git a/tests/test-pearl-format.el b/tests/test-pearl-format.el
index 1119734..53af2e7 100644
--- a/tests/test-pearl-format.el
+++ b/tests/test-pearl-format.el
@@ -113,12 +113,12 @@ so these tests no longer need a state mapping bound."
(let ((out (pearl--format-issue-as-org-entry
'(:id "u" :identifier "ENG-1" :title "Fix [URGENT] bug"
:priority 1 :state (:name "Todo")))))
- (should (string-match-p "^\\*\\* TODO \\[#A\\] ENG-1: Fix URGENT Bug$" out))
- ;; the title provenance hash is of the displayed (stripped + cased) title
+ (should (string-match-p "^\\*\\* TODO \\[#A\\] ENG-1: Fix URGENT bug$" out))
+ ;; the title provenance hash is of the displayed (stripped, verbatim) title
;; without the identifier prefix, so a later no-op title sync matches the
;; heading and never clobbers brackets or pushes the prefix
(should (string-match-p
- (format "^:LINEAR-TITLE-SHA256: +%s$" (secure-hash 'sha256 "Fix URGENT Bug"))
+ (format "^:LINEAR-TITLE-SHA256: +%s$" (secure-hash 'sha256 "Fix URGENT bug"))
out)))))
;;; build-org-content
diff --git a/tests/test-pearl-heading.el b/tests/test-pearl-heading.el
index 2675518..a780143 100644
--- a/tests/test-pearl-heading.el
+++ b/tests/test-pearl-heading.el
@@ -82,8 +82,18 @@
;;; rendering on / off
-(ert-deftest test-pearl-format-heading-defaults-prefix-and-title-case ()
- "By default the heading carries the identifier prefix and a title-cased title."
+(ert-deftest test-pearl-format-heading-default-prefix-verbatim-title ()
+ "By default the heading carries the identifier prefix and the verbatim title,
+matching Linear's own capitalization."
+ (let ((pearl-show-identifier-in-heading t)
+ (pearl-title-case-headings nil))
+ (let ((out (pearl--format-issue-as-org-entry
+ '(:id "u" :identifier "SE-401" :title "fix the refresh bug"
+ :priority 2 :state (:name "Todo")))))
+ (should (string-match-p "^\\*\\* TODO \\[#B\\] SE-401: fix the refresh bug$" out)))))
+
+(ert-deftest test-pearl-format-heading-title-case-opt-in ()
+ "With title-casing opted in, the title is smart-cased; the prefix is unaffected."
(let ((pearl-show-identifier-in-heading t)
(pearl-title-case-headings t))
(let ((out (pearl--format-issue-as-org-entry
diff --git a/tests/test-pearl-output.el b/tests/test-pearl-output.el
index 58cd446..a838bfb 100644
--- a/tests/test-pearl-output.el
+++ b/tests/test-pearl-output.el
@@ -68,12 +68,13 @@
(should (string-match-p "^#\\+title:" out))
(should-not (string-match-p "^\\*\\*\\* " out))))
-(ert-deftest test-pearl-build-org-content-title-cases-view-name ()
- "The view name in the file title is title-cased when title-casing is on, raw when off."
+(ert-deftest test-pearl-build-org-content-title-uses-view-name-verbatim ()
+ "The view name in the file title matches Linear's capitalization verbatim,
+independent of `pearl-title-case-headings' (which governs issue headings only)."
(let ((case-fold-search nil) ; strict so casing actually matters
(source '(:type filter :name "my open bugs" :filter nil)))
(let ((pearl-title-case-headings t))
- (should (string-match-p "^#\\+title: Linear — My Open Bugs$"
+ (should (string-match-p "^#\\+title: Linear — my open bugs$"
(pearl--build-org-content '() source))))
(let ((pearl-title-case-headings nil))
(should (string-match-p "^#\\+title: Linear — my open bugs$"
diff --git a/tests/test-pearl-save.el b/tests/test-pearl-save.el
index 90e55cd..2f7b312 100644
--- a/tests/test-pearl-save.el
+++ b/tests/test-pearl-save.el
@@ -43,7 +43,11 @@
;; `org-entry-put', which in batch makes the cache reconcile pathologically
;; (seconds per call). Disabling it keeps the suite fast and deterministic;
;; production never touches this binding.
+ ;; Title-case the rendered heading so these save-path tests can search for the
+ ;; cased title; the save logic hashes the rendered form regardless of casing,
+ ;; so the opt-in binding doesn't change what's under test.
`(let ((pearl-comment-sort-order 'newest-first)
+ (pearl-title-case-headings t)
(org-element-use-cache nil))
(with-temp-buffer
(insert (pearl--format-issue-as-org-entry ,issue))