On Sun, Jan 25 2026, Kristoffer Balintona wrote:

> Hello,
>
> Attached are patches that incorporate your feedback and are rebased on
> master. I also think I fixed a few bugs along the way.
>
> I manually tested file+headline, file+olp, and file+olp+datetree, with
> only <file-spec> and no headline/olp as well as with both <file-spec>
> with headline/olp. I also took care to test each with :prepend nil and
> :prepend t, as well as file+olp+datetree without an existing datetree
> and with at least one existing datetree.
>
> Additionally, I updated the :type of 'org-capture-templates'
> accordingly. I've never dealt with a :type as complex as the one for
> 'org-capture-templates', so I'm not sure if my solution is the best one.
> But it seems not to throw any errors or warnings, and the widgets in the
> Customize menu seem right.
>
> On Sun, May 18 2025, Ihor Radchenko wrote:
>
>>> -                 For non-unique headings, the full outline path is safer
>>> +                 For non-unique headings, the full outline path is
>>> +                 safer.  If no headings are given, or if
>>> +                 function-returning-list-of-strings or
>>> +                 symbol-containing-list-of-strings return nil, the entry
>>> +                 will be inserted at the end of <file-spec> on top
>>> +                 level.
>>
>> You missed optional :prepend property in capture templates.
>> See `org-capture-place-entry'. The location will depend on :prepend
>> being t/nil unless you explicitly set :exact-position when processing
>> the capture template.
>
> I see. So you're saying that the current implementation is fine but the
> description of resulting behavior is inaccurate, namely, that the entry
> can be inserted at the beginning of the target location if :prepend is
> non-nil?
>
> In the attached patches, I've updated the docstrings and comments to
> reflect this understanding. Let me know if I misunderstood the change(s)
> you wanted from me.
>
>>> -                (m (org-find-olp (cons expanded-file-path
>>> -                                  (apply #'org-capture-expand-olp 
>>> expanded-file-path outline-path)))))
>>> +                (expanded-olp (apply #'org-capture-expand-olp 
>>> expanded-file-path outline-path))
>>> +                ;; If expanded-olp is nil, then create the datetree at
>>> +                ;; the end of the file specified by expanded-file-path
>>> +                (m (if expanded-olp
>>> +                       (org-find-olp (cons expanded-file-path 
>>> expanded-olp))
>>> +                     (set-buffer (org-capture-target-buffer 
>>> expanded-file-path))
>>> +                     (save-restriction (widen) (point-max-marker)))))
>>
>> I am not sure if I understand the purpose of the last line here.
>
> I've added a comment to explain the reasoning. Does the comment suffice
> in its explanation? Or have I misunderstood something.
>
>>> +    (let* ((expanded-file-path (org-capture-expand-file path))
>>> +                (expanded-olp (apply #'org-capture-expand-olp 
>>> expanded-file-path outline-path))
>>> +                ;; If expanded-olp is nil, then file-level-datetree-p
>>> +                ;; is non-nil and the datetree should be created at
>>> +                ;; the end of the file specified by expanded-file-path
>>
>> There is no guarantee that it will be created at the end.
>
> Ditto my response to your feedback mentioning :prepend.
>
>>> +         (pcase headline
>>> +           ((pred not) (goto-char (point-max)))
>>
>> Nitpick: `null' is a bit more comprehensive as predicate name.
>
> Done.
>
> --
> Kind regards,
> Kristoffer
> From c4a0fe797ca01030a607e6d2bd7397596a1b3d3e Mon Sep 17 00:00:00 2001
> From: Kristoffer Balintona <[email protected]>
> Date: Fri, 9 May 2025 11:40:13 -0500
> Subject: [PATCH 1/2] lisp/org-capture.el: Accept optional and nil olp for
>  file+olp+datetree and file+olp
>
> * lisp/org-capture.el (org-capture-expand-olp): When the supplied olp
> is nil, org-capture-expand-olp now returns nil.
> (org-capture-set-target-location): The file+olp+datetree and file+olp
> target specifications now accept a nil or omitted olp.  Update the
> docstring to reflect the new behaviors.
> * testing/lisp/test-org-capture.el (org-capture-expand-olp): Add test
> case for org-capture-expand-olp now accepting a nil olp.
> ---
>  lisp/org-capture.el              | 137 +++++++++++++++++++++----------
>  testing/lisp/test-org-capture.el |   7 ++
>  2 files changed, 99 insertions(+), 45 deletions(-)
>
> diff --git a/lisp/org-capture.el b/lisp/org-capture.el
> index 3c8040854..24c2d51f0 100644
> --- a/lisp/org-capture.el
> +++ b/lisp/org-capture.el
> @@ -207,21 +207,33 @@ target       Specification of where the captured item 
> should be placed.
>               (file+headline <file-spec> symbol-containing-string)
>                   Fast configuration if the target heading is unique in the 
> file
>
> +             (file+olp <file-spec>)
>               (file+olp <file-spec> \"Level 1 heading\" \"Level 2\" ...)
>               (file+olp <file-spec> function-returning-list-of-strings)
>               (file+olp <file-spec> symbol-containing-list-of-strings)
> -                 For non-unique headings, the full outline path is safer
> +                 For non-unique headings, the full outline path is
> +                 safer.  The entry is created at the outline path (a
> +                 list of strings denoting headlines).  If no outline
> +                 path is specified or if the outline path specification
> +                 is nil, the entry will be inserted at the top level of
> +                 <file-spec>.
>
>               (file+regexp  <file-spec> \"regexp to find location\")
>                   File to the entry containing matching regexp
>
> +             (file+olp+datetree <file-spec>)
>               (file+olp+datetree <file-spec> \"Level 1 heading\" ...)
>               (file+olp+datetree <file-spec> 
> function-returning-list-of-strings)
>               (file+olp+datetree <file-spec> 
> symbol-containing-list-of-strings)
> -                 Will create a heading in a date tree for today's date.
> -                 If no heading is given, the tree will be on top level.
> -                 To prompt for date instead of using TODAY, use the
> -                 :time-prompt property.  To create a week-tree, use the
> +                 Will create an entry in a datetree under the specified
> +                 outline path. for today\\='s date.  If no outline path
> +                 is given or if the outline path specification is nil,
> +                 the entry will be inserted into the first already
> +                 existing top level datetree in <file-spec> or, if no
> +                 top level datetree exists, a newly created datetree at
> +                 the end of <file-spec>.  To prompt for a date instead
> +                 of using today\\='s date, use the
> +                 :time-prompt property.  To create a week-tree,use the
>                   :tree-type property.
>
>               (file+function <file-spec> function-finding-location)
> @@ -451,11 +463,10 @@ you can escape ambiguous cases with a backward slash, 
> e.g., \\%i."
>                               (file :tag "Literal")
>                               (function :tag "Function")
>                               (variable :tag "Variable")))
> -        (olp-variants '(choice :tag "Outline path"
> -                               (repeat :tag "Outline path" :inline t
> -                                    (string :tag "Headline"))
> -                            (function :tag "Function")
> -                            (variable :tag "Variable"))))
> +        (olp-variants-choices '((repeat :tag "Outline path" :inline t
> +                                     (string :tag "Headline"))
> +                             (function :tag "Function")
> +                             (variable :tag "Variable"))))
>      `(repeat
>        (choice :value ("" "" entry (file "~/org/notes.org") "")
>             (list :tag "Multikey description"
> @@ -484,20 +495,22 @@ you can escape ambiguous cases with a backward slash, 
> e.g., \\%i."
>                                         (string   :tag "Headline")
>                                         (function :tag "Function")
>                                         (variable :tag "Variable")))
> -                         (list :tag "File & Outline path"
> -                               (const :format "" file+olp)
> -                               ,file-variants
> -                               ,olp-variants)
> +                         (list :tag "File [ & Outline path ]"
> +                                  (const :format "" file+olp)
> +                                  ,file-variants
> +                                  (choice :tag "Outline path"
> +                                          (const :tag "Top level" nil)
> +                                          ,@olp-variants-choices))
>                           (list :tag "File & Regexp"
>                                 (const :format "" file+regexp)
>                                 ,file-variants
>                                 (regexp :tag "  Regexp"))
> -                         (list :tag "File [ & Outline path ] & Date tree"
> -                               (const :format "" file+olp+datetree)
> -                               ,file-variants
> -                                  ,(append
> -                                    olp-variants
> -                                    '((const :tag "Date tree at top level" 
> nil))))
> +                            (list :tag "File [ & Outline path ] & Date tree"
> +                                  (const :format "" file+olp+datetree)
> +                                  ,file-variants
> +                                  (choice :tag "Outline path"
> +                                          (const :tag "Date tree at top 
> level" nil)
> +                                          ,@olp-variants-choices))
>                           (list :tag "File & function"
>                                 (const :format "" file+function)
>                                 ,file-variants
> @@ -1094,10 +1107,29 @@ Store them in the capture property list."
>          (unless (bolp) (insert "\n"))
>          (insert "* " headline "\n")
>          (forward-line -1)))
> -     (`(file+olp ,path . ,(and outline-path (guard outline-path)))
> +     ((or `(file+olp ,path)
> +             `(file+olp ,path . ,outline-path))
>        (let* ((expanded-file-path (org-capture-expand-file path))
> -                (m (org-find-olp (cons expanded-file-path
> -                                    (apply #'org-capture-expand-olp 
> expanded-file-path outline-path)))))
> +                (expanded-olp (apply #'org-capture-expand-olp 
> expanded-file-path outline-path))
> +                ;; Vary behavior depending on whether expanded-olp is
> +                ;; nil or non-nil.  If expanded-olp is non-nil, then
> +                ;; create the entry at that outline path.  If
> +                ;; expanded-olp is nil (no olp is provided), then
> +                ;; create the entry in the expanded-file-path file
> +                (m (if expanded-olp
> +                       (org-find-olp (cons expanded-file-path expanded-olp))
> +                     (set-buffer (org-capture-target-buffer 
> expanded-file-path))
> +                     ;; If expanded-olp is nil or omitted, then the
> +                     ;; user is essentially using the form (file
> +                     ;; <file-spec>), so behave as such by setting
> +                     ;; `target-entry-p' to nil
> +                     (setq target-entry-p nil)
> +                     ;; Return a marker pointing to the end of the
> +                     ;; file to cause the new entry to be created
> +                     ;; there.  We widen first to consider the
> +                     ;; possibility of the buffer already being open
> +                     ;; and narrowed by the user
> +                     (save-restriction (widen) (point-max-marker)))))
>          (set-buffer (marker-buffer m))
>          (org-capture-put-target-region-and-position)
>          (widen)
> @@ -1116,13 +1148,24 @@ Store them in the capture property list."
>          (org-capture-put :exact-position (point))
>          (setq target-entry-p
>                (and (derived-mode-p 'org-mode) (org-at-heading-p)))))
> -     (`(file+olp+datetree ,path . ,outline-path)
> -      (let ((m (if outline-path
> -                   (let ((expanded-file-path (org-capture-expand-file path)))
> -                        (org-find-olp (cons expanded-file-path
> -                                         (apply #'org-capture-expand-olp 
> expanded-file-path outline-path))))
> -                 (set-buffer (org-capture-target-buffer path))
> -                 (point-marker))))
> +     ((or `(file+olp+datetree ,path)
> +             `(file+olp+datetree ,path . ,outline-path))
> +         (let* ((expanded-file-path (org-capture-expand-file path))
> +                (expanded-olp (apply #'org-capture-expand-olp 
> expanded-file-path outline-path))
> +                ;; Vary behavior depending on whether expanded-olp is
> +                ;; nil or non-nil.  If expanded-olp is non-nil, then
> +                ;; create the datetree at that outline path.  If
> +                ;; expanded-olp is nil (no olp is provided), then
> +                ;; create the datetree in the expanded-file-path file
> +                (m (if expanded-olp
> +                       (org-find-olp (cons expanded-file-path expanded-olp))
> +                     (set-buffer (org-capture-target-buffer 
> expanded-file-path))
> +                     ;; Return a marker pointing to the end of the
> +                     ;; buffer to cause the new entry to be created
> +                     ;; there.  We widen first to consider the
> +                     ;; possibility of the buffer already being open
> +                     ;; and narrowed by the user
> +                     (save-restriction (widen) (point-max-marker)))))
>          (set-buffer (marker-buffer m))
>          (org-capture-put-target-region-and-position)
>          (widen)
> @@ -1183,7 +1226,7 @@ Store them in the capture property list."
>              (org-today))))
>           ;; the following is the keep-restriction argument for
>           ;; org-datetree-find-date-create
> -         (when outline-path 'subtree-at-point))))
> +            (when expanded-olp 'subtree-at-point))))
>       (`(file+function ,path ,(and function (pred functionp)))
>        (set-buffer (org-capture-target-buffer path))
>        (org-capture-put-target-region-and-position)
> @@ -1232,20 +1275,24 @@ an error."
>
>  (defun org-capture-expand-olp (file &rest olp)
>    "Expand functions, symbols and outline paths in FILE for OLP.
> -When OLP is a function, call it with no arguments while the current
> -buffer is the FILE-visiting buffer.  When it is a variable, return its
> -value.  When it is a list of string, return it.  In any other case,
> -signal an error."
> -  (let* ((first (car olp))
> -         (final-olp (cond ((not (memq nil (mapcar #'stringp olp))) olp)
> -                          ((and (not (cdr olp)) (functionp first))
> -                           (with-current-buffer (find-file-noselect file)
> -                             (funcall first)))
> -                          ((and (not (cdr olp)) (symbolp first) (boundp 
> first))
> -                           (symbol-value first))
> -                          (t nil))))
> -    (or final-olp
> -        (error "org-capture: Invalid outline path target: %S" olp))))
> +Return a list of strings representing an outline path (OLP) in FILE.
> +
> +The behavior of this function is as follows:
> +- When OLP is a function, call it with no arguments while the current
> +  buffer is the FILE-visiting buffer.
> +- When it is a variable, return its value.
> +- When it is a list of strings, return that list.
> +- When OLP is nil, return nil.
> +In any other case, signal an error."
> +  (let* ((first (car olp)))
> +    (cond ((and (= 1 (length olp)) (null first)) nil)
> +          ((not (memq nil (mapcar #'stringp olp))) olp)
> +          ((and (not (cdr olp)) (functionp first))
> +           (with-current-buffer (find-file-noselect file)
> +             (funcall first)))
> +          ((and (not (cdr olp)) (symbolp first) (boundp first))
> +           (symbol-value first))
> +          (t (error "org-capture: Invalid outline path target: %S" olp)))))
>
>  (defun org-capture-expand-file (file)
>    "Expand functions, symbols and file names for FILE.
> diff --git a/testing/lisp/test-org-capture.el 
> b/testing/lisp/test-org-capture.el
> index 6b49a2df7..cc1c07480 100644
> --- a/testing/lisp/test-org-capture.el
> +++ b/testing/lisp/test-org-capture.el
> @@ -1091,6 +1091,13 @@ before\nglobal-before\nafter\nglobal-after"
>      '("A" "B" "C")
>      (org-test-with-temp-text-in-file ""
>        (org-capture-expand-olp buffer-file-name "A" "B" "C"))))
> +  ;; `org-capture-expand-olp' should return nil if the outline path is
> +  ;; nil
> +  (should
> +   (equal
> +    nil
> +    (org-test-with-temp-text-in-file ""
> +      (org-capture-expand-olp buffer-file-name nil))))
>    ;; The current buffer during the funcall of the lambda is the temporary
>    ;; test file.
>    (should
> --
> 2.52.0
>
> From 2f7268f0795c5b437ffc4f6651c54a480ce09b59 Mon Sep 17 00:00:00 2001
> From: Kristoffer Balintona <[email protected]>
> Date: Fri, 9 May 2025 11:40:13 -0500
> Subject: [PATCH 2/2] lisp/org-capture.el: Accept optional or nil headline for
>  the file+headline
>
> * lisp/org-capture.el (org-capture-expand-headline): When the supplied
> headline is nil, org-capture-expand-olp now returns nil.
> (org-capture-set-target-location): The file+headline target
> specification now accepts a nil or omitted headline.  In either case,
> insert the capture entry at the top level of the file.  Update
> docstring to reflect the new behaviors.
> * testing/lisp/test-org-capture.el
> (test-org-capture/org-capture-expand-headline): Add new test for
> org-capture-expand-headline that tests these new behaviors as well as
> several previously existing ones.
> ---
>  lisp/org-capture.el              | 58 ++++++++++++++++++++------------
>  testing/lisp/test-org-capture.el | 42 +++++++++++++++++++++++
>  2 files changed, 79 insertions(+), 21 deletions(-)
>
> diff --git a/lisp/org-capture.el b/lisp/org-capture.el
> index 24c2d51f0..2a027c175 100644
> --- a/lisp/org-capture.el
> +++ b/lisp/org-capture.el
> @@ -202,10 +202,16 @@ target       Specification of where the captured item 
> should be placed.
>               (id \"id of existing Org entry\")
>                   File as child of this entry, or in the body of the entry
>
> +             (file+headline <file-spec>)
>               (file+headline <file-spec> \"node headline\")
>               (file+headline <file-spec> function-returning-string)
>               (file+headline <file-spec> symbol-containing-string)
> -                 Fast configuration if the target heading is unique in the 
> file
> +                 Fast configuration if the target heading is unique in
> +                 the file.  The entry is created at the headline
> +                 specified by a string, symbol, or function.  If no
> +                 headline is provided or if the headline specification
> +                 is nil, the entry will be inserted at the top level of
> +                 <file-spec>.
>
>               (file+olp <file-spec>)
>               (file+olp <file-spec> \"Level 1 heading\" \"Level 2\" ...)
> @@ -492,6 +498,7 @@ you can escape ambiguous cases with a backward slash, 
> e.g., \\%i."
>                                 (const :format "" file+headline)
>                                 ,file-variants
>                                 (choice :tag "Headline"
> +                                          (const :tag "Top level" nil)
>                                         (string   :tag "Headline")
>                                         (function :tag "Function")
>                                         (variable :tag "Variable")))
> @@ -1084,7 +1091,8 @@ Store them in the capture property list."
>           (org-capture-put-target-region-and-position)
>           (goto-char position))
>          (_ (error "Cannot find target ID \"%s\"" id))))
> -     (`(file+headline ,path ,headline)
> +        ((or `(file+headline ,path)
> +             `(file+headline ,path ,headline))
>        (set-buffer (org-capture-target-buffer path))
>        ;; Org expects the target file to be in Org mode, otherwise
>        ;; it throws an error.  However, the default notes files
> @@ -1099,15 +1107,19 @@ Store them in the capture property list."
>        (widen)
>        (goto-char (point-min))
>           (setq headline (org-capture-expand-headline headline))
> -      (if (re-search-forward (format org-complex-heading-regexp-format
> -                                     (regexp-quote headline))
> -                             nil t)
> -          (forward-line 0)
> -        (goto-char (point-max))
> -        (unless (bolp) (insert "\n"))
> -        (insert "* " headline "\n")
> -        (forward-line -1)))
> -     ((or `(file+olp ,path)
> +         (pcase headline
> +           ((pred null) (goto-char (point-max)))
> +           ((pred stringp)
> +            (re-search-forward (format org-complex-heading-regexp-format
> +                                       (regexp-quote headline))
> +                               nil t)
> +            (forward-line 0))
> +           (_
> +            (goto-char (point-max))
> +            (unless (bolp) (insert "\n"))
> +            (insert "* " headline "\n")
> +            (forward-line -1))))
> +        ((or `(file+olp ,path)
>               `(file+olp ,path . ,outline-path))
>        (let* ((expanded-file-path (org-capture-expand-file path))
>                  (expanded-olp (apply #'org-capture-expand-olp 
> expanded-file-path outline-path))
> @@ -1262,16 +1274,20 @@ Store them in the capture property list."
>
>  (defun org-capture-expand-headline (headline)
>    "Expand functions, symbols and headline names for HEADLINE.
> -When HEADLINE is a function, call it.  When it is a variable, return
> -its value.  When it is a string, return it.  In any other case, signal
> -an error."
> -  (let* ((final-headline (cond ((stringp headline) headline)
> -                               ((functionp headline) (funcall headline))
> -                               ((and (symbolp headline) (boundp headline))
> -                                (symbol-value headline))
> -                               (t nil))))
> -    (or final-headline
> -        (error "org-capture: Invalid headline target: %S" headline))))
> +Return a string representing a headline.
> +
> +The behavior of this function is as follows:
> +- When HEADLINE is a function, call it.
> +- When it is a variable, return its value.
> +- When it is a string, return that string.
> +- When headline is nil, return nil.
> +In any other case, signal an error."
> +  (cond ((null headline) nil)
> +        ((stringp headline) headline)
> +        ((functionp headline) (funcall headline))
> +        ((and (symbolp headline) (boundp headline))
> +         (symbol-value headline))
> +        (t (error "org-capture: Invalid headline target: %S" headline))))
>
>  (defun org-capture-expand-olp (file &rest olp)
>    "Expand functions, symbols and outline paths in FILE for OLP.
> diff --git a/testing/lisp/test-org-capture.el 
> b/testing/lisp/test-org-capture.el
> index cc1c07480..05421c4ae 100644
> --- a/testing/lisp/test-org-capture.el
> +++ b/testing/lisp/test-org-capture.el
> @@ -1083,6 +1083,48 @@ before\nglobal-before\nafter\nglobal-after"
>                (org-capture nil "t")
>                (buffer-string))))))
>
> +(ert-deftest test-org-capture/org-capture-expand-headline ()
> +  "Test `org-capture-expand-headline'."
> +  ;; `org-capture-expand-headline' should return nil when headline is
> +  ;; nil
> +  (should
> +   (equal
> +    nil
> +    (let ((file (make-temp-file "org-test")))
> +      (unwind-protect
> +          (org-capture-expand-headline nil)
> +        (delete-file file)))))
> +  ;; `org-capture-expand-headline' should return headline if it is a
> +  ;; string
> +  (should
> +   (equal
> +    "A"
> +    (let ((file (make-temp-file "org-test")))
> +      (unwind-protect
> +          (org-capture-expand-headline "A")
> +        (delete-file file)))))
> +  ;; `org-capture-expand-headline' should evaluate headline if it is a
> +  ;; function and return its value
> +  (should
> +   (equal
> +    "A"
> +    (let ((file (make-temp-file "org-test")))
> +      (unwind-protect
> +          (org-capture-expand-headline (lambda () "A"))
> +        (delete-file file)))))
> +  ;; `org-capture-expand-headline' should return the value of headline
> +  ;; if it is a symbol
> +  (should
> +   (equal
> +    "A"
> +    (let ((file (make-temp-file "org-test")))
> +      (unwind-protect
> +          (progn
> +            (setq temp "A")
> +            (org-capture-expand-headline 'temp))
> +        (makunbound 'temp)
> +        (delete-file file))))))
> +
>  (ert-deftest test-org-capture/org-capture-expand-olp ()
>    "Test org-capture-expand-olp."
>    ;; `org-capture-expand-olp' accepts inlined outline path.

Actually, just discovered a big regarding the updated :type of
'org-capture-templates': the order of elements in 'olp-variants-choices'
(formerly 'olp-variants') meant variables and functions weren't properly
being matched against, resulting in warnings when the user customized
'org-capture-templates' to use functions or symbols in the file+olp or
file+datetree target specifications. (I think this bug should also be
present in the master branch?)

Attached are the updated patches for what I think are the appropriate
fixes (reordering the types in the 'choice' :type constructor such that
more specific types are first).

-- 
Kind regards,
Kristoffer
From ab76a552551150058d05ca896ee85db5996be114 Mon Sep 17 00:00:00 2001
From: Kristoffer Balintona <[email protected]>
Date: Fri, 9 May 2025 11:40:13 -0500
Subject: [PATCH 1/2] lisp/org-capture.el: Accept optional and nil olp for
 file+olp+datetree and file+olp

* lisp/org-capture.el (org-capture-expand-olp): When the supplied olp
is nil, org-capture-expand-olp now returns nil.
(org-capture-set-target-location): The file+olp+datetree and file+olp
target specifications now accept a nil or omitted olp.  Update the
docstring to reflect the new behaviors.
* testing/lisp/test-org-capture.el (org-capture-expand-olp): Add test
case for org-capture-expand-olp now accepting a nil olp.
---
 lisp/org-capture.el              | 137 +++++++++++++++++++++----------
 testing/lisp/test-org-capture.el |   7 ++
 2 files changed, 99 insertions(+), 45 deletions(-)

diff --git a/lisp/org-capture.el b/lisp/org-capture.el
index 3c8040854..b69b4b174 100644
--- a/lisp/org-capture.el
+++ b/lisp/org-capture.el
@@ -207,21 +207,33 @@ target       Specification of where the captured item should be placed.
              (file+headline <file-spec> symbol-containing-string)
                  Fast configuration if the target heading is unique in the file
 
+             (file+olp <file-spec>)
              (file+olp <file-spec> \"Level 1 heading\" \"Level 2\" ...)
              (file+olp <file-spec> function-returning-list-of-strings)
              (file+olp <file-spec> symbol-containing-list-of-strings)
-                 For non-unique headings, the full outline path is safer
+                 For non-unique headings, the full outline path is
+                 safer.  The entry is created at the outline path (a
+                 list of strings denoting headlines).  If no outline
+                 path is specified or if the outline path specification
+                 is nil, the entry will be inserted at the top level of
+                 <file-spec>.
 
              (file+regexp  <file-spec> \"regexp to find location\")
                  File to the entry containing matching regexp
 
+             (file+olp+datetree <file-spec>)
              (file+olp+datetree <file-spec> \"Level 1 heading\" ...)
              (file+olp+datetree <file-spec> function-returning-list-of-strings)
              (file+olp+datetree <file-spec> symbol-containing-list-of-strings)
-                 Will create a heading in a date tree for today's date.
-                 If no heading is given, the tree will be on top level.
-                 To prompt for date instead of using TODAY, use the
-                 :time-prompt property.  To create a week-tree, use the
+                 Will create an entry in a datetree under the specified
+                 outline path for today\\='s date.  If no outline path
+                 is given or if the outline path specification is nil,
+                 the entry will be inserted into the first already
+                 existing top level datetree in <file-spec> or, if no
+                 top level datetree exists, a newly created datetree at
+                 the end of <file-spec>.  To prompt for a date instead
+                 of using today\\='s date, use the
+                 :time-prompt property.  To create a week-tree,use the
                  :tree-type property.
 
              (file+function <file-spec> function-finding-location)
@@ -451,11 +463,10 @@ you can escape ambiguous cases with a backward slash, e.g., \\%i."
 				(file :tag "Literal")
 				(function :tag "Function")
 				(variable :tag "Variable")))
-        (olp-variants '(choice :tag "Outline path"
-                               (repeat :tag "Outline path" :inline t
-				       (string :tag "Headline"))
-			       (function :tag "Function")
-			       (variable :tag "Variable"))))
+        (olp-variants-choices '((function :tag "Function")
+                                (variable :tag "Variable")
+                                (repeat :tag "Outline path" :inline t
+                                        (string :tag "Headline")))))
     `(repeat
       (choice :value ("" "" entry (file "~/org/notes.org") "")
 	      (list :tag "Multikey description"
@@ -484,20 +495,22 @@ you can escape ambiguous cases with a backward slash, e.g., \\%i."
 				          (string   :tag "Headline")
 				          (function :tag "Function")
 				          (variable :tag "Variable")))
-			    (list :tag "File & Outline path"
-				  (const :format "" file+olp)
-				  ,file-variants
-				  ,olp-variants)
+			    (list :tag "File [ & Outline path ]"
+                                  (const :format "" file+olp)
+                                  ,file-variants
+                                  (choice :tag "Outline path"
+                                          (const :tag "Top level" nil)
+                                          ,@olp-variants-choices))
 			    (list :tag "File & Regexp"
 				  (const :format "" file+regexp)
 				  ,file-variants
 				  (regexp :tag "  Regexp"))
-			    (list :tag "File [ & Outline path ] & Date tree"
-				  (const :format "" file+olp+datetree)
-				  ,file-variants
-                                  ,(append
-                                    olp-variants
-                                    '((const :tag "Date tree at top level" nil))))
+                            (list :tag "File [ & Outline path ] & Date tree"
+                                  (const :format "" file+olp+datetree)
+                                  ,file-variants
+                                  (choice :tag "Outline path"
+                                          (const :tag "Date tree at top level" nil)
+                                          ,@olp-variants-choices))
 			    (list :tag "File & function"
 				  (const :format "" file+function)
 				  ,file-variants
@@ -1094,10 +1107,29 @@ Store them in the capture property list."
 	   (unless (bolp) (insert "\n"))
 	   (insert "* " headline "\n")
 	   (forward-line -1)))
-	(`(file+olp ,path . ,(and outline-path (guard outline-path)))
+	((or `(file+olp ,path)
+             `(file+olp ,path . ,outline-path))
 	 (let* ((expanded-file-path (org-capture-expand-file path))
-                (m (org-find-olp (cons expanded-file-path
-				       (apply #'org-capture-expand-olp expanded-file-path outline-path)))))
+                (expanded-olp (apply #'org-capture-expand-olp expanded-file-path outline-path))
+                ;; Vary behavior depending on whether expanded-olp is
+                ;; nil or non-nil.  If expanded-olp is non-nil, then
+                ;; create the entry at that outline path.  If
+                ;; expanded-olp is nil (no olp is provided), then
+                ;; create the entry in the expanded-file-path file
+                (m (if expanded-olp
+                       (org-find-olp (cons expanded-file-path expanded-olp))
+                     (set-buffer (org-capture-target-buffer expanded-file-path))
+                     ;; If expanded-olp is nil or omitted, then the
+                     ;; user is essentially using the form (file
+                     ;; <file-spec>), so behave as such by setting
+                     ;; `target-entry-p' to nil
+                     (setq target-entry-p nil)
+                     ;; Return a marker pointing to the end of the
+                     ;; file to cause the new entry to be created
+                     ;; there.  We widen first to consider the
+                     ;; possibility of the buffer already being open
+                     ;; and narrowed by the user
+                     (save-restriction (widen) (point-max-marker)))))
 	   (set-buffer (marker-buffer m))
 	   (org-capture-put-target-region-and-position)
 	   (widen)
@@ -1116,13 +1148,24 @@ Store them in the capture property list."
 	   (org-capture-put :exact-position (point))
 	   (setq target-entry-p
 		 (and (derived-mode-p 'org-mode) (org-at-heading-p)))))
-	(`(file+olp+datetree ,path . ,outline-path)
-	 (let ((m (if outline-path
-		      (let ((expanded-file-path (org-capture-expand-file path)))
-                        (org-find-olp (cons expanded-file-path
-					    (apply #'org-capture-expand-olp expanded-file-path outline-path))))
-		    (set-buffer (org-capture-target-buffer path))
-		    (point-marker))))
+	((or `(file+olp+datetree ,path)
+             `(file+olp+datetree ,path . ,outline-path))
+         (let* ((expanded-file-path (org-capture-expand-file path))
+                (expanded-olp (apply #'org-capture-expand-olp expanded-file-path outline-path))
+                ;; Vary behavior depending on whether expanded-olp is
+                ;; nil or non-nil.  If expanded-olp is non-nil, then
+                ;; create the datetree at that outline path.  If
+                ;; expanded-olp is nil (no olp is provided), then
+                ;; create the datetree in the expanded-file-path file
+                (m (if expanded-olp
+                       (org-find-olp (cons expanded-file-path expanded-olp))
+                     (set-buffer (org-capture-target-buffer expanded-file-path))
+                     ;; Return a marker pointing to the end of the
+                     ;; buffer to cause the new entry to be created
+                     ;; there.  We widen first to consider the
+                     ;; possibility of the buffer already being open
+                     ;; and narrowed by the user
+                     (save-restriction (widen) (point-max-marker)))))
 	   (set-buffer (marker-buffer m))
 	   (org-capture-put-target-region-and-position)
 	   (widen)
@@ -1183,7 +1226,7 @@ Store them in the capture property list."
 	       (org-today))))
 	    ;; the following is the keep-restriction argument for
 	    ;; org-datetree-find-date-create
-	    (when outline-path 'subtree-at-point))))
+            (when expanded-olp 'subtree-at-point))))
 	(`(file+function ,path ,(and function (pred functionp)))
 	 (set-buffer (org-capture-target-buffer path))
 	 (org-capture-put-target-region-and-position)
@@ -1232,20 +1275,24 @@ an error."
 
 (defun org-capture-expand-olp (file &rest olp)
   "Expand functions, symbols and outline paths in FILE for OLP.
-When OLP is a function, call it with no arguments while the current
-buffer is the FILE-visiting buffer.  When it is a variable, return its
-value.  When it is a list of string, return it.  In any other case,
-signal an error."
-  (let* ((first (car olp))
-         (final-olp (cond ((not (memq nil (mapcar #'stringp olp))) olp)
-                          ((and (not (cdr olp)) (functionp first))
-                           (with-current-buffer (find-file-noselect file)
-                             (funcall first)))
-                          ((and (not (cdr olp)) (symbolp first) (boundp first))
-                           (symbol-value first))
-                          (t nil))))
-    (or final-olp
-        (error "org-capture: Invalid outline path target: %S" olp))))
+Return a list of strings representing an outline path (OLP) in FILE.
+
+The behavior of this function is as follows:
+- When OLP is a function, call it with no arguments while the current
+  buffer is the FILE-visiting buffer.
+- When it is a variable, return its value.
+- When it is a list of strings, return that list.
+- When OLP is nil, return nil.
+In any other case, signal an error."
+  (let* ((first (car olp)))
+    (cond ((and (= 1 (length olp)) (null first)) nil)
+          ((not (memq nil (mapcar #'stringp olp))) olp)
+          ((and (not (cdr olp)) (functionp first))
+           (with-current-buffer (find-file-noselect file)
+             (funcall first)))
+          ((and (not (cdr olp)) (symbolp first) (boundp first))
+           (symbol-value first))
+          (t (error "org-capture: Invalid outline path target: %S" olp)))))
 
 (defun org-capture-expand-file (file)
   "Expand functions, symbols and file names for FILE.
diff --git a/testing/lisp/test-org-capture.el b/testing/lisp/test-org-capture.el
index 6b49a2df7..cc1c07480 100644
--- a/testing/lisp/test-org-capture.el
+++ b/testing/lisp/test-org-capture.el
@@ -1091,6 +1091,13 @@ before\nglobal-before\nafter\nglobal-after"
     '("A" "B" "C")
     (org-test-with-temp-text-in-file ""
       (org-capture-expand-olp buffer-file-name "A" "B" "C"))))
+  ;; `org-capture-expand-olp' should return nil if the outline path is
+  ;; nil
+  (should
+   (equal
+    nil
+    (org-test-with-temp-text-in-file ""
+      (org-capture-expand-olp buffer-file-name nil))))
   ;; The current buffer during the funcall of the lambda is the temporary
   ;; test file.
   (should
-- 
2.52.0

From af911b46ee9ef03f7bd2f4bd5b6b607dd9bd9f77 Mon Sep 17 00:00:00 2001
From: Kristoffer Balintona <[email protected]>
Date: Fri, 9 May 2025 11:40:13 -0500
Subject: [PATCH 2/2] lisp/org-capture.el: Accept optional or nil headline for
 the file+headline

* lisp/org-capture.el (org-capture-expand-headline): When the supplied
headline is nil, org-capture-expand-olp now returns nil.
(org-capture-set-target-location): The file+headline target
specification now accepts a nil or omitted headline.  In either case,
insert the capture entry at the top level of the file.  Update
docstring to reflect the new behaviors.
* testing/lisp/test-org-capture.el
(test-org-capture/org-capture-expand-headline): Add new test for
org-capture-expand-headline that tests these new behaviors as well as
several previously existing ones.
---
 lisp/org-capture.el              | 58 ++++++++++++++++++++------------
 testing/lisp/test-org-capture.el | 42 +++++++++++++++++++++++
 2 files changed, 79 insertions(+), 21 deletions(-)

diff --git a/lisp/org-capture.el b/lisp/org-capture.el
index b69b4b174..7e45b4a63 100644
--- a/lisp/org-capture.el
+++ b/lisp/org-capture.el
@@ -202,10 +202,16 @@ target       Specification of where the captured item should be placed.
              (id \"id of existing Org entry\")
                  File as child of this entry, or in the body of the entry
 
+             (file+headline <file-spec>)
              (file+headline <file-spec> \"node headline\")
              (file+headline <file-spec> function-returning-string)
              (file+headline <file-spec> symbol-containing-string)
-                 Fast configuration if the target heading is unique in the file
+                 Fast configuration if the target heading is unique in
+                 the file.  The entry is created at the headline
+                 specified by a string, symbol, or function.  If no
+                 headline is provided or if the headline specification
+                 is nil, the entry will be inserted at the top level of
+                 <file-spec>.
 
              (file+olp <file-spec>)
              (file+olp <file-spec> \"Level 1 heading\" \"Level 2\" ...)
@@ -492,6 +498,7 @@ you can escape ambiguous cases with a backward slash, e.g., \\%i."
 				  (const :format "" file+headline)
 				  ,file-variants
 				  (choice :tag "Headline"
+                                          (const :tag "Top level" nil)
 				          (string   :tag "Headline")
 				          (function :tag "Function")
 				          (variable :tag "Variable")))
@@ -1084,7 +1091,8 @@ Store them in the capture property list."
 	    (org-capture-put-target-region-and-position)
 	    (goto-char position))
 	   (_ (error "Cannot find target ID \"%s\"" id))))
-	(`(file+headline ,path ,headline)
+        ((or `(file+headline ,path)
+             `(file+headline ,path ,headline))
 	 (set-buffer (org-capture-target-buffer path))
 	 ;; Org expects the target file to be in Org mode, otherwise
 	 ;; it throws an error.  However, the default notes files
@@ -1099,15 +1107,19 @@ Store them in the capture property list."
 	 (widen)
 	 (goto-char (point-min))
          (setq headline (org-capture-expand-headline headline))
-	 (if (re-search-forward (format org-complex-heading-regexp-format
-					(regexp-quote headline))
-				nil t)
-	     (forward-line 0)
-	   (goto-char (point-max))
-	   (unless (bolp) (insert "\n"))
-	   (insert "* " headline "\n")
-	   (forward-line -1)))
-	((or `(file+olp ,path)
+         (pcase headline
+           ((pred null) (goto-char (point-max)))
+           ((pred stringp)
+            (re-search-forward (format org-complex-heading-regexp-format
+                                       (regexp-quote headline))
+                               nil t)
+            (forward-line 0))
+           (_
+            (goto-char (point-max))
+            (unless (bolp) (insert "\n"))
+            (insert "* " headline "\n")
+            (forward-line -1))))
+        ((or `(file+olp ,path)
              `(file+olp ,path . ,outline-path))
 	 (let* ((expanded-file-path (org-capture-expand-file path))
                 (expanded-olp (apply #'org-capture-expand-olp expanded-file-path outline-path))
@@ -1262,16 +1274,20 @@ Store them in the capture property list."
 
 (defun org-capture-expand-headline (headline)
   "Expand functions, symbols and headline names for HEADLINE.
-When HEADLINE is a function, call it.  When it is a variable, return
-its value.  When it is a string, return it.  In any other case, signal
-an error."
-  (let* ((final-headline (cond ((stringp headline) headline)
-                               ((functionp headline) (funcall headline))
-                               ((and (symbolp headline) (boundp headline))
-                                (symbol-value headline))
-                               (t nil))))
-    (or final-headline
-        (error "org-capture: Invalid headline target: %S" headline))))
+Return a string representing a headline.
+
+The behavior of this function is as follows:
+- When HEADLINE is a function, call it.
+- When it is a variable, return its value.
+- When it is a string, return that string.
+- When headline is nil, return nil.
+In any other case, signal an error."
+  (cond ((null headline) nil)
+        ((stringp headline) headline)
+        ((functionp headline) (funcall headline))
+        ((and (symbolp headline) (boundp headline))
+         (symbol-value headline))
+        (t (error "org-capture: Invalid headline target: %S" headline))))
 
 (defun org-capture-expand-olp (file &rest olp)
   "Expand functions, symbols and outline paths in FILE for OLP.
diff --git a/testing/lisp/test-org-capture.el b/testing/lisp/test-org-capture.el
index cc1c07480..05421c4ae 100644
--- a/testing/lisp/test-org-capture.el
+++ b/testing/lisp/test-org-capture.el
@@ -1083,6 +1083,48 @@ before\nglobal-before\nafter\nglobal-after"
               (org-capture nil "t")
               (buffer-string))))))
 
+(ert-deftest test-org-capture/org-capture-expand-headline ()
+  "Test `org-capture-expand-headline'."
+  ;; `org-capture-expand-headline' should return nil when headline is
+  ;; nil
+  (should
+   (equal
+    nil
+    (let ((file (make-temp-file "org-test")))
+      (unwind-protect
+          (org-capture-expand-headline nil)
+        (delete-file file)))))
+  ;; `org-capture-expand-headline' should return headline if it is a
+  ;; string
+  (should
+   (equal
+    "A"
+    (let ((file (make-temp-file "org-test")))
+      (unwind-protect
+          (org-capture-expand-headline "A")
+        (delete-file file)))))
+  ;; `org-capture-expand-headline' should evaluate headline if it is a
+  ;; function and return its value
+  (should
+   (equal
+    "A"
+    (let ((file (make-temp-file "org-test")))
+      (unwind-protect
+          (org-capture-expand-headline (lambda () "A"))
+        (delete-file file)))))
+  ;; `org-capture-expand-headline' should return the value of headline
+  ;; if it is a symbol
+  (should
+   (equal
+    "A"
+    (let ((file (make-temp-file "org-test")))
+      (unwind-protect
+          (progn
+            (setq temp "A")
+            (org-capture-expand-headline 'temp))
+        (makunbound 'temp)
+        (delete-file file))))))
+
 (ert-deftest test-org-capture/org-capture-expand-olp ()
   "Test org-capture-expand-olp."
   ;; `org-capture-expand-olp' accepts inlined outline path.
-- 
2.52.0

Reply via email to