doct
is a function that provides an alternative, declarative syntax for describing Org capture templates.
(defun my-org-template-hook ()
(when (string= (org-capture-get :key t) "p1")
(message "\"First Child\" selected.")))
(add-hook 'org-capture-mode-hook 'my-org-template-hook)
(setq org-capture-templates
'(("p" "Parent")
("p1" "First Child" entry (file+headline "~/example.org" "One")
"* TODO %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?"
:prepend t)
("p2" "Second Child" entry (file+headline "~/example.org" "Two")
"* NEXT %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?"
:prepend t)
("p3" "Third Child" entry (file+headline "~/example.org" "Three")
"* MAYBE %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?"
:prepend t)))
becomes:
(setq org-capture-templates
(doct '(("Parent" :keys "p"
:file "~/example.org"
:prepend t
:template ("* %{todo-state} %^{Description}"
":PROPERTIES:"
":Created: %U"
":END:"
"%?")
:children (("First Child" :keys "1"
:headline "One"
:todo-state "TODO"
:hook (lambda () (message "\"First Child\" selected.")))
("Second Child" :keys "2"
:headline "Two"
:todo-state "NEXT")
("Third Child" :keys "3"
:headline "Three"
:todo-state "MAYBE"))))))
- add doct-add-to function Add support for “here” target (Org 9.7)
- add doct-add-to function
- drop support for recursive
%{keyword}
expansion.
- drop support for
%doct(KEYWORD)
syntax. Replaced with%{keyword}
doct-get
now accepts an optional parameter to determine whether to queryorg-capture-plist
ororg-capture-current-plist
.- adopt semver
Try it with the straight package manager:
(straight-use-package 'doct)
or use-package:
(use-package doct
:ensure t
;;recommended: defer until calling doct
:commands (doct))
Download doct
and make sure it is in your load-path.
Require it in your init file.
(require 'doct)
(doct declarations)
doct
expects a declaration or a list of declarations as its sole argument
and converts it into a list of `org-capture-templates`.
Each declaration is either a child, parent, or group.
A child declaration must have:
- a name
- a
:keys
string - a template type
- a target
- a template
and may also have:
- hook functions defined with the hook keywords
- contexts declared via the
:contexts
keyword - additional
:KEY VAL
arguments
A parent declaration must have:
- a name
- a
:keys
string - a list of
:children
and may also have additional properties inherited by its children.
A group is a special kind of parent declaration.
Its children inherit its properties, but it is not added to the template selection menu.
Its name must be the :group
keyword.
It may optionally have a descriptive string for the value of :group
.
It must not have a :keys
value.
(doct '(("Work" :keys "w" :file "~/org/work.org" :children
((:group "Clocked" :clock-in t :children
(("Phone Call" :keys "p" :template "* Phone call with %?")
("Meeting" :keys "m" :template "* Meeting with %?")))
("Browsing" :keys "b" :template "* Browsing %x")))))
returns:
(("w" "Work")
("wp" "Phone Call" entry (file "~/org/work.org") "* Phone call with %?" :clock-in t)
("wm" "Meeting" entry (file "~/org/work.org") "* Meeting with %?" :clock-in t)
("wb" "Browsing" entry (file "~/org/work.org") "* Browsing %x"))
Every declaration must define a name. Unless it is a group , it must also define a :keys
value.
The name is the first value in the declaration. The :keys
keyword defines the keys to access the template
from the capture menu.
(doct '(("example" :keys "e" ...)))
returns:
(("e" "example" ...))
The :children
keyword defines a parent’s children.
Its value may be a single declaration or a list of declarations.
The parent’s :keys
prefix each child’s :keys
.
(doct '(("parent" :keys "p"
:children
(("child" :keys "c"
:children
(("grandchild" :keys "g"
:file ""
:type plain
:template "test")))))))
returns:
(("p" "parent") ("pc" "child") ("pcg" "grandchild" plain (file "") "test"))
A child inherits its ancestors’ properties. It may optionally override an inherited property by specifying that property directly.
For example:
(doct '(("Grandparent" :keys "g"
:file "example.org"
:children ("Parent" :keys "p"
:children ("Child" :keys "c")))))
The “Child” template inherits its :file
property from the “Grandparent” declaration.
The “Parent” declaration could override this value:
(doct '(("Grandparent" :keys "g"
:file "example.org"
:children ("Parent" :keys "p"
:file "overridden.org"
:children ("Child" :keys "c")))))
And the “Child” would have its :file
property set to “overridden.org”.
The :type
keyword defines the template’s entry type and accepts the following symbols:
- entry
- An Org node with a headline. The template becomes a child of the target entry or a top level entry.
- item
- A plain list item, placed in the first plain list at the target location.
- checkitem
- A checkbox item. Same as plain list item only it uses a different default template.
- table-line
- A new line in the first table at target location.
- plain
- Text inserted as is.
doct-default-entry-type
defines the entry type when the :type
keyword is not provided.
For example, with doct-default-entry-type
set to entry
(the default):
(doct '(("example"
:keys "e"
:type entry
:file "")))
And
(doct '(("example"
:keys "e"
:file "")))
Both return:
(("e" "example" entry (file "") nil))
The target defines the location of the inserted template text.
The first keyword declared in the following group exclusively sets the target.
The :file
keyword is not necessary for these.
- :id “id of existing Org entry”
- File as child of this entry, or in the body of the entry (see org-id-get-create and the Org Mode Manual)
- :clock t
- File to the currently clocked entry
- :function (lambda () ;visit file and move point to desired location…)
-
This keyword is exclusive when used without the
:file
keyword. It is responsible for finding the proper file and location to insert the capture item. If:file
defines a target file, then the function is only responsible for moving point to the desired location within that file.
(doct '(("example"
:keys "e"
:type entry
:clock t
;;ignored because clock is first
:function (lambda () (ignore))
;;also ignored
:id "1")))
returns:
(("e" "example" entry (clock) nil))
The :file
keyword defines the target file for the capture template.
It may be:
- a string:
(doct ... :file "/path/to/target.org")
;;empty string defaults to `org-default-notes-file'
(doct ... :file "")
- a function:
;;lambda
(doct ... :file (lambda () (concat (read-string "Org Capture Path: ") ".org")))
;;or a function symbol
(doct ... :file my/get-file-path)
- or a variable:
(doct ... :file my/file-path)
The following keywords refine the target file location:
- :headline “node headline”
- File under unique heading in target file.
- :olp (“Level 1 heading” “Level 2 heading”…)
-
Define the full outline in the target file.
- :datetree nil|t
-
Requires use of the
:file
keyword. If:datetree
has a non-nil value, create a date tree for today’s date. If:olp
is given, the date tree is added under that heading path. Use a non-nil:time-prompt
property to prompt for a different date. Set the:tree-type
property to the symbolweek
create a week tree instead of the default month tree. - :regexp “regexp describing location”
-
File to the entry matching regexp in target file
- :function location-finding-function
-
If used in addition to the
:file
keyword, the value should be a function that finds the desired location in that file. If used as an exclusive keyword (see above), the function must locate both the target file and move point to the desired location.
The :template
keyword defines the template for creating the capture item.
It may be either a string, list of strings, or a function.
doct
joins the list with new lines.
A function must return the template text.
(doct '((... :template ("Test" "One" "Two"))))
returns:
((... "Test\nOne\nTwo"))
The :template-file
keyword defines a file containing the text of the template.
For example:
(doct '((... :template-file "~/org/templates/template.txt")))
will use the text of template.txt as the template string.
The first keyword declared overrides any additional template declarations.
Key-value pairs define additional options.
(doct '((... :immediate-finish t)))
returns:
((... :immediate-finish t))
see the Org Mode Manual for a full list of additional options.
doct
stores unrecognized keywords on the template’s org-capture-plist as members of the doct-custom plist.
This makes a template’s metadata accessible during capture.
See %{KEYWORD} Expansion for details on using that data.
The :custom
keyword accepts a plist.
doct copies the plist’s values to the doct-custom plist.
This is only necessary if you wish to use a keyword which doct already uses.
For example:
(doct '(("Music Gear" :keys "m" :file ""
:custom (:keys "Moog"))))
returns:
(#1="m" #2="Music Gear" entry (file #3="") nil
:doct (#2# :keys #1# :file #3# :custom #4=(:keys "Moog") :doct-custom #4#))))
:template
may include a keyword’s value during capture.
The syntax is similar to other, built-in “%-escapes”.
%{KEYWORD}
will insert the value declared with :KEYWORD
in the declaration.
For example, with:
(doct '(("Parent" :keys "p"
:file ""
:template "* %{todo-state} %?"
:children (("One" :keys "1" :todo-state "TODO")
("Two" :keys "2" :todo-state "IDEA")))))
Each child template has its :todo-state
value expanded in the inherited :template.
Values should be strings, functions or nil.
(doct '(("%{string}" :keys "s" :type plain :file ""
:string "string"
:template "%{string}")))
Is replaced with:
"string"
(doct '(("%{fn}" :keys "f" :type plain :file ""
:fn (lambda () "string returned from function")
:template "%{fn}")))
Is replaced with:
"string returned from function"
(doct '(("%{nil}" :keys "f" :type plain :file ""
:nil nil
:template "%{nil}")))
Is replaced with the empty string
""
Custom keywords take precedence over other declaration keywords. For example, with:
(doct '(("Music Gear" :keys "m" :file "" :type plain
:custom (:keys "Moog")
:template "%{keys}")))
The “Music Gear” template expands to “Moog” instead of “m”. Nil values expand to an empty string.
Adding the following hook keywords in a declaration adds its value to the appropriate org-capture hook. The value may be a function or a variable.
- :hook function
-
- org-capture-mode-hook
-
Runs FUNCTION when entering the org-capture-mode minor mode.
- :prepare-finalize function
-
- org-capture-prepare-finalize-hook
-
Runs FUNCTION before the finalization starts. The capture buffer is current and narrowed.
- :before-finalize function
-
- org-capture-before-finalize-hook
-
Runs FUNCTION right before a capture process finalizes. The capture buffer is still current and widened to the entire buffer.
- :after-finalize function
-
- org-capture-after-finalize-hook
- Runs FUNCTION right after a capture process finalizes. Suitable for window cleanup.
For example:
(doct `(("example"
:keys "e"
:file ""
:hook ,(defun my/fn ()
(ignore)))))
runs my/fn
during the org-capture-mode-hook
when selecting the “example” template.
The :contexts
keyword defines contextual rules for a template.
Its value may be a single contextual rule or a list of rules.
The following keywords are available to create contextual rules:
- :in-buffer regexp
- Show template when REGEXP matches the current buffer’s name.
(doct '(("Only in *scratch*" :keys "n" :file "" :contexts (:in-buffer "^\\*scratch\\*$"))))
- :unless-buffer regexp
- Show template unless REGEXP matches the current buffer’s name.
(doct '(("Except in *scratch*" :keys "n" :file "" :contexts (:unless-buffer "^\\*scratch\\*$"))))
- :in-file regexp
- Show template when REGEXP matches the current buffer’s file name.
(doct '(("Only in work.org" :keys "n" :file "" :contexts (:in-file "work\\.org$"))))
- :unless-file regexp
- Show template unless REGEXP matches the current buffer’s file name.
(doct '(("Except in work.org" :keys "n" :file "" :contexts (:unless-file "work\\.org$"))))
- :in-mode regexp
- Show template when REGEXP matches the current buffer’s major mode.
(doct '(("Only in org-mode" :keys "n" :file "" :contexts (:in-mode "org-mode"))))
- :unless-mode regexp
- Show template unless REGEXP matches the current buffer’s major mode.
(doct '(("Except in org-mode" :keys "n" :file "" :contexts (:unless-mode "org-mode"))))
- :when condition
- Show template when CONDITION evaluates to a non-nil value. Condition may be a function or a single form.
(doct '(("Show when my/predicate-p returns t" :keys "n" :file "" :contexts (:when my/predicate-p))))
(doct '(("1/3 chance of showing" :keys "n" :file "" :contexts (:when (= 2 (random 3))))))
- :unless condition
- Show template when CONDITION evaluates to a nil value. Condition may be a function or a single form.
(doct '(("Show when my/predicate-p returns nil" :keys "n" :file "" :contexts (:unless my/predicate-p))))
(doct '(("2/3 chance of showing" :keys "n" :file "" :contexts (:unless (= 2 (random 3))))))
- :function function
- Show template when FUNCTION returns non-nil. The function is not passed any arguments.
(doct '(("Between 9AM and 5PM" :keys "n" :file ""
:contexts (:function (lambda () (<= 9 (string-to-number (format-time-string "%H")) 17)))))))
Adding :keys
to a rule does the same as above, but remaps the template’s keys to the template with keys matching the :keys
string.
For example:
(doct '(("In *scratch* remapped to t, else use original template"
:keys "n" :file "" :contexts ((:unless-buffer "^\\*scratch\\*$" :keys "n")
(:in-buffer "^\\*scratch\\*$" :keys "t")))))
The above rule keywords, spare :function
, :when
, and :unless
may also take a list of strings for their values.
(doct '(("Only in org-mode or emacs-lisp-mode" :keys "n" :file ""
:contexts (:in-mode ("org-mode" "emacs-lisp-mode")))))
:disabled
keyword to t disables a template.
The template’s declaration is not error checked.
This can be useful if you don’t have the time to deal with an error right away.
For example:
(doct '((:group "All" :file "" :children
((:group "Enabled" :children
(("One" :keys "1")
("Two" :keys "2")
("Three" :keys "3")))
(:group "Disabled" :disabled t :children
(("Four" :keys 4)
("Five" :keys 5)
("Six" :keys 6)))))))
returns:
(("1" "One" entry (file "") nil)
("2" "Two" entry (file "") nil)
("3" "Three" entry (file "") nil))
Normally template “Four” would throw an error because its :keys
are not a string.
The :warn
keyword disables doct
’s warnings on a per-declaration basis.
For example:
(let ((doct-warnings t))
(doct '(("Ignore unbound symbol warnings" :keys "i"
:warn (:not unbound)
:file unbound-variable
:function unbound-function)
("Warn here, though" :keys "w"
:file unbound-variable
:function unbound-function))))
For global control of warnings and an explanation of accepted values see doct-warnings
in Custom Variables.
(doct-add-to list declarations &optional append)
Return a copy of LIST
with converted DECLARATIONS
added.
If APPEND
is non-nil, add to back of LIST
.
Otherwise, add to the front of LIST
.
DECLARATIONS
are passed to doct
and must be the same of the same form doct
accepts.
e.g.
;; `org-capture-templates' set earlier elsewhere...
(setq org-capture-templates
(doct-add-to org-capture-templates
'("example" :keys "e" ...)
'append))
doct
supports the following variables for customization:
- doct-default-entry-type ‘entry
-
The default template entry type.
It can be overridden on a per-declaration basis by using the
:type
keyword. - doct-after-conversion-functions
-
Abnormal hook run after converting declarations to templates.
Hook functions run with the list of templates as their only argument.
The templates are not flattened at this point and are of the form:
(((parent) (child)...)...).
- doct-warnings
-
When non-nil, doct will issue warnings. Valid values are:
- t
- warn in all cases
- nil
- do not warn
Or a list containing any of the following symbols:
- unbound
- warn when a symbol is unbound during conversion
- template-keyword
- warn when %{KEYWORD} is not found on the declaration during conversion.
- template-keyword-type
- warn when %{KEYWORD} expansion does not return a string.
- template-entry-type
- warn when the expanded template string does not match the capture template’s entry type
- template-file
-
warn when the
:template-file
’s file is not found during conversion - option-type
- warn when additional options are not the proper type
If the list’s first element is the
:not
keyword, the list of warnings is disabled. It can be overridden on a per-declaration basis with the :warn keyword.”For example:
(let ((doct-warnings t)) (doct '(("Ignore unbound symbol warnings" :keys "i" :warn (:not unbound) :file unbound-variable :function unbound-function) ("Warn here, though" :keys "w" :file unbound-variable :function unbound-function))))
Pull/feature requests, code review, angry comments are all welcome.
Please add a test to the test suite if you introduce any changes.
Thanks, nv