Org document property-drawers

Add functionality to define property-blocks on document level, in
addition to at headline level.

* doc/org-manual.org:
* etc/ORG-NEWS: Document new functionality.

* lisp/org.el (org-keyword-regexp): Define constant instead of
  hardcoding.
(org-file-properties): Renamed, see next line.
(org-keyword-properties): Renamed from above.  Due to the fact that
  properties can be defined for the whole document using property
  drawers this local variable needs a rename to make its name less
  ambigous.
(org-refresh-properties, org-refresh-property, org-entry-properties)
(org-refresh-category-properties, org-get-property-block)
(org-entry-get-with-inheritance, org-entry-put)
(org-insert-property-drawer, org-end-of-subtree): Made to work before
  first headline.
(org-at-property-block-p): New function to validate if point is at the
start of a property block.
(org-property-global-value): Renamed, see next line.
(org-property-global-or-keyword-value): Renamed from above to match
  its functionality better.
(org-back-to-heading-or-point-min): New function to make a document
  work as a level 0 node in the outline.
(org-at-keyword-p): Predicate function to answer to if we're currently
at a keyword line or not.
(org-up-heading-or-point-min): New function to make a document work as
a level 0 node in the outline.

* lisp/org-element.el (org-element--current-element): Can now detect
  property-blocks before first headline according to it's positional
  rules.

* lisp/org-attach.el (org-attach): Make it possible to call the
  attachment dispatcher also before the first headline, since document
  property drawers make attachments possible for the whole document
  now.

* lisp/org-capture.el: Modified only due to rename of function in
  org.el.

* lisp/org-compat.el (org-file-properties)
(org-property-global-value): Renamed functions declared obsolete.

* testing/lisp/test-org.el (org/insert-property-drawer)
(org/set-property, org/delete-property, org/delete-property-globally):
  Additions of tests to check if they work before first headline.
(org/at-property-p, org/at-property-block-p, org/get-property-block)
(org/entry-get, org/refresh-properties): New tests

* testing/examples/property-inheritance.org: Switch from
  property-keywords to a property-drawer in the testfile.
  Functionality should be the same, but now using a document drawer
  instead of property-keywords.

  Reason for switching is that I'd like us to slowly depricate
  property-keywords.

* testing/lisp/test-org-element.el:

* contrib/lisp/ox-taskjuggler.el: A comment is modified only due to
  rename of function in org.el.
This commit is contained in:
Gustav Wikström 2019-05-26 22:13:09 +02:00
parent aa8500505f
commit 1bdff9f73d
11 changed files with 535 additions and 117 deletions

View File

@ -137,7 +137,7 @@
;; :END:
;;
;;;; * TODO
;; - Look at org-file-properties, org-global-properties and
;; - Look at org-keyword-properties, org-global-properties and
;; org-global-properties-fixed
;; - What about property inheritance and org-property-inherit-p?
;; - Use TYPE_TODO as an way to assign resources

View File

@ -4957,7 +4957,7 @@ with many examples, see [[*Matching tags and properties]].
A property is a key-value pair associated with an entry. Properties
can be set so they are associated with a single entry, with every
entry in a tree, or with every entry in an Org file.
entry in a tree, or with the whole buffer.
There are two main applications for properties in Org mode. First,
properties are like tags, but with a value. Imagine maintaining
@ -5021,8 +5021,12 @@ disks in a box like this:
:END:
#+end_example
If you want to set properties that can be inherited by any entry in
a file, use a line like:
Properties can be inserted on buffer level. That means they apply
before the first headline and can be inherited by all entries in a
file. Property blocks defined before first headline needs to be
located at the top of the buffer, allowing only comments above.
Properties can also be defined using lines like:
#+cindex: @samp{_ALL} suffix, in properties
#+cindex: @samp{PROPERTY}, keyword
@ -5087,7 +5091,8 @@ The following commands help to work with properties:
#+findex: org-insert-drawer
Insert a property drawer into the current entry. The drawer is
inserted early in the entry, but after the lines with planning
information like deadlines.
information like deadlines. If before first headline the drawer is
inserted at the top of the drawer after any potential comments.
- {{{kbd(C-c C-c)}}} (~org-property-action~) ::
@ -5310,11 +5315,6 @@ done by defining a column format line.
:DESCRIPTION: Where defined, where valid?
:END:
To define a column format for an entire file, use a line like:
#+cindex: @samp{COLUMNS}, keyword
: #+COLUMNS: %25ITEM %TAGS %PRIORITY %TODO
To specify a format that only applies to a specific tree, add
a =COLUMNS= property to the top node of that tree, for example:
@ -5325,6 +5325,13 @@ a =COLUMNS= property to the top node of that tree, for example:
:END:
#+end_example
A =COLUMNS= property within a property drawer before first headline
will apply to the entire file. As an addition to property drawers,
keywords can also be defined for an entire file using a line like:
#+cindex: @samp{COLUMNS}, keyword
: #+COLUMNS: %25ITEM %TAGS %PRIORITY %TODO
If a =COLUMNS= property is present in an entry, it defines columns for
the entry itself, and for the entire subtree below it. Since the
column definition is part of the hierarchical structure of the

View File

@ -10,6 +10,16 @@ See the end of the file for license conditions.
Please send Org bug reports to mailto:emacs-orgmode@gnu.org.
* Version Next
** New features
*** Property drawers before first headline, outline level 0
Property drawers will now work before first headline and Org mode is
moving more towards making things before the first headline behave
just as if it was at outline level 0. Inheritance for properties will
work also for this level. In other words; defining things in a
property drawer before the first headline will make them "inheritable"
for all headlines.
* Version 9.3
** Incompatible changes
@ -2171,7 +2181,7 @@ without changing the headline.
*** Hierarchies of tags
The functionality of nesting tags in hierarchies is added to org-mode.
The functionality of nesting tags in hierarchies is added to Org mode.
This is the generalization of what was previously called "Tag groups"
in the manual. That term is now changed to "Tag hierarchy".

View File

@ -244,7 +244,7 @@ Shows a list of commands and prompts for another key to execute a command."
(when marker
(set-buffer (marker-buffer marker))
(goto-char marker))
(org-back-to-heading t)
(org-back-to-heading-or-point-min t)
(save-excursion
(save-window-excursion
(unless org-attach-expert

View File

@ -1736,11 +1736,11 @@ The template may still contain \"%?\" for cursor positioning."
(_ (error "Invalid `org-capture--clipboards' value: %S"
org-capture--clipboards)))))
("p"
;; We remove file properties inherited from
;; We remove keyword properties inherited from
;; target buffer so `org-read-property-value' has
;; a chance to find allowed values in sub-trees
;; from the target buffer.
(setq-local org-file-properties nil)
(setq-local org-keyword-properties nil)
(let* ((origin (set-marker (make-marker)
(org-capture-get :pos)
(org-capture-get :buffer)))

View File

@ -556,6 +556,12 @@ use of this function is for the stuck project list."
(define-obsolete-function-alias 'org-make-link-regexps
'org-link-make-regexps "Org 9.3")
(define-obsolete-function-alias 'org-file-properties
'org-keyword-properties "Org 9.3")
(define-obsolete-function-alias 'org-property-global-value
'org-property-global-or-keyword-value "Org 9.3")
(define-obsolete-variable-alias 'org-angle-link-re
'org-link-angle-re "Org 9.3")

View File

@ -3910,7 +3910,10 @@ element it has to parse."
;; LaTeX Environment.
((looking-at org-element--latex-begin-environment)
(org-element-latex-environment-parser limit affiliated))
;; Drawer and Property Drawer.
;; Property drawer (before first headline, else it's catched above).
((org-at-property-block-p)
(org-element-property-drawer-parser limit))
;; Drawer.
((looking-at org-drawer-regexp)
(org-element-drawer-parser limit affiliated))
;; Fixed Width

View File

@ -348,6 +348,9 @@ FULL is given."
;;; Syntax Constants
;;;; Keyword
(defconst org-keyword-regexp "^[ \t]*#\\+\\(\\S-+?\\):[ \t]*\\(.*\\)$"
"Regular expression for keyword-lines")
;;;; Block
@ -3180,8 +3183,13 @@ This list will be combined with the constant `org-global-properties-fixed'.
The entries in this list are cons cells where the car is a property
name and cdr is a string with the value.
You can set buffer-local values for the same purpose in the variable
`org-file-properties' this by adding lines like
Buffer local properties are added either by a document property drawer
:PROPERTIES:
:NAME: VALUE
:END:
or by adding lines like
#+PROPERTY: NAME VALUE"
:group 'org-properties
@ -3189,10 +3197,14 @@ You can set buffer-local values for the same purpose in the variable
(cons (string :tag "Property")
(string :tag "Value"))))
(defvar-local org-file-properties nil
(defvar-local org-keyword-properties nil
"List of property/value pairs that can be inherited by any entry.
Valid for the current buffer.
This variable is populated from #+PROPERTY lines.")
Valid for the current buffer. This variable is populated from
#+PROPERTY lines.
Note that properties are defined also in property drawers.
Properties defined there will take precedence over properties
defined as keywords.")
(defgroup org-agenda nil
"Options concerning agenda views in Org mode."
@ -3201,11 +3213,18 @@ This variable is populated from #+PROPERTY lines.")
(defvar-local org-category nil
"Variable used by Org files to set a category for agenda display.
Such files should use a file variable to set it, for example
There are multiple ways to set the category. One way is to set
it in the document property drawer. For example:
:PROPERTIES:
:CATEGORY: ELisp
:END:
Other ways to define it is as an emacs file variable, for example
# -*- mode: org; org-category: \"ELisp\"
or contain a special line
or for the file to contain a special line:
#+CATEGORY: ELisp
@ -4338,8 +4357,8 @@ related expressions."
(setq org-tag-groups-alist
(org-tag-alist-to-groups org-current-tag-alist))
(unless tags-only
;; File properties.
(setq-local org-file-properties (cdr (assq 'property alist)))
;; Properties.
(setq-local org-keyword-properties (cdr (assq 'property alist)))
;; Archive location.
(let ((archive (cdr (assq 'archive alist))))
(when archive (setq-local org-archive-location archive)))
@ -4347,9 +4366,9 @@ related expressions."
(let ((cat (org-string-nw-p (cdr (assq 'category alist)))))
(when cat
(setq-local org-category (intern cat))
(setq-local org-file-properties
(setq-local org-keyword-properties
(org--update-property-plist
"CATEGORY" cat org-file-properties))))
"CATEGORY" cat org-keyword-properties))))
;; Columns.
(let ((column (cdr (assq 'columns alist))))
(when column (setq-local org-columns-default-format column)))
@ -8267,13 +8286,14 @@ the value of the drawer property."
(inhibit-read-only t)
(inherit? (org-property-inherit-p dprop))
(property-re (org-re-property (concat (regexp-quote dprop) "\\+?") t))
(global (and inherit? (org--property-global-value dprop nil))))
(global-or-keyword (and inherit?
(org--property-global-or-keyword-value dprop nil))))
(with-silent-modifications
(org-with-point-at 1
;; Set global values (e.g., values defined through
;; "#+PROPERTY:" keywords) to the whole buffer.
(when global (put-text-property (point-min) (point-max) tprop global))
;; Set local values.
;; Set global and keyword based values to the whole buffer.
(when global-or-keyword
(put-text-property (point-min) (point-max) tprop global-or-keyword))
;; Set values based on property-drawers throughout the document.
(while (re-search-forward property-re nil t)
(when (org-at-property-p)
(org-refresh-property tprop (org-entry-get (point) dprop) inherit?))
@ -8281,21 +8301,29 @@ the value of the drawer property."
(defun org-refresh-property (tprop p &optional inherit)
"Refresh the buffer text property TPROP from the drawer property P.
The refresh happens only for the current headline, or the whole
sub-tree if optional argument INHERIT is non-nil."
(unless (org-before-first-heading-p)
(save-excursion
(org-back-to-heading t)
(let ((start (point))
(end (save-excursion
(if inherit (org-end-of-subtree t t)
(or (outline-next-heading) (point-max))))))
(if (symbolp tprop)
;; TPROP is a text property symbol.
(put-text-property start end tprop p)
;; TPROP is an alist with (property . function) elements.
(pcase-dolist (`(,prop . ,f) tprop)
(put-text-property start end prop (funcall f p))))))))
The refresh happens only for the current entry, or the whole
sub-tree if optional argument INHERIT is non-nil.
If point is before first headline, the function applies to the
part before the first headline. In that particular case, when
optional argument INHERIT is non-nil, it refreshes properties for
the whole buffer."
(save-excursion
(org-back-to-heading-or-point-min t)
(let ((start (point))
(end (save-excursion
(cond ((and inherit (org-before-first-heading-p))
(point-max))
(inherit
(org-end-of-subtree t t))
((outline-next-heading))
((point-max))))))
(if (symbolp tprop)
;; TPROP is a text property symbol.
(put-text-property start end tprop p)
;; TPROP is an alist with (property . function) elements.
(pcase-dolist (`(,prop . ,f) tprop)
(put-text-property start end prop (funcall f p)))))))
(defun org-refresh-category-properties ()
"Refresh category text properties in the buffer."
@ -8311,9 +8339,9 @@ sub-tree if optional argument INHERIT is non-nil."
(t org-category))))
(with-silent-modifications
(org-with-wide-buffer
;; Set buffer-wide category. Search last #+CATEGORY keyword.
;; This is the default category for the buffer. If none is
;; found, fall-back to `org-category' or buffer file name.
;; Set buffer-wide property from keyword. Search last #+CATEGORY
;; keyword. If none is found, fall-back to `org-category' or
;; buffer file name, or set it by the document property drawer.
(put-text-property
(point-min) (point-max)
'org-category
@ -8325,15 +8353,20 @@ sub-tree if optional argument INHERIT is non-nil."
(throw 'buffer-category
(org-element-property :value element)))))
default-category))
;; Set sub-tree specific categories.
;; Set categories from the document property drawer or
;; property drawers in the outline. If category is found in
;; the property drawer for the whole buffer that value
;; overrides the keyword-based value set above.
(goto-char (point-min))
(let ((regexp (org-re-property "CATEGORY")))
(while (re-search-forward regexp nil t)
(let ((value (match-string-no-properties 3)))
(when (org-at-property-p)
(put-text-property
(save-excursion (org-back-to-heading t) (point))
(save-excursion (org-end-of-subtree t t) (point))
(save-excursion (org-back-to-heading-or-point-min t))
(save-excursion (if (org-before-first-heading-p)
(point-max)
(org-end-of-subtree t t)))
'org-category
value)))))))))
@ -12919,30 +12952,45 @@ Modifications are made by side-effect. Return new alist."
(defun org-get-property-block (&optional beg force)
"Return the (beg . end) range of the body of the property drawer.
BEG is the beginning of the current subtree, or of the part
before the first headline. If it is not given, it will be found.
If the drawer does not exist, create it if FORCE is non-nil, or
return nil."
BEG is the beginning of the current subtree or the beginning of
the document if before the first headline. If it is not given,
it will be found. If the drawer does not exist, create it if
FORCE is non-nil, or return nil."
(org-with-wide-buffer
(when beg (goto-char beg))
(unless (org-before-first-heading-p)
(let ((beg (cond (beg)
(let ((beg (cond (beg (goto-char beg))
((or (not (featurep 'org-inlinetask))
(org-inlinetask-in-task-p))
(org-back-to-heading t))
(t (org-with-limited-levels (org-back-to-heading t))))))
(forward-line)
(when (looking-at-p org-planning-line-re) (forward-line))
(cond ((looking-at org-property-drawer-re)
(forward-line)
(cons (point) (progn (goto-char (match-end 0))
(line-beginning-position))))
(force
(goto-char beg)
(org-insert-property-drawer)
(let ((pos (save-excursion (search-forward ":END:")
(line-beginning-position))))
(cons pos pos))))))))
(org-back-to-heading-or-point-min t) (point))
(t (org-with-limited-levels
(org-back-to-heading-or-point-min t))
(point)))))
;; Move point to its position according to its positional rules.
(cond ((org-before-first-heading-p)
(while (and (org-at-comment-p) (bolp)) (forward-line)))
(t (forward-line)
(when (looking-at-p org-planning-line-re) (forward-line))))
(cond ((looking-at org-property-drawer-re)
(forward-line)
(cons (point) (progn (goto-char (match-end 0))
(line-beginning-position))))
(force
(goto-char beg)
(org-insert-property-drawer)
(let ((pos (save-excursion (re-search-forward org-property-drawer-re)
(line-beginning-position))))
(cons pos pos)))))))
(defun org-at-property-block-p ()
"Return t when point is at the first line of a property drawer.
The property drawer is validated according to its positional
rules using `org-get-property-block'."
(save-excursion
(beginning-of-line)
(and (looking-at org-property-start-re)
(forward-line)
(let ((property-drawer (org-get-property-block)))
(and property-drawer
(= (point) (car property-drawer)))))))
(defun org-at-property-p ()
"Non-nil when point is inside a property drawer.
@ -13027,7 +13075,7 @@ Return value is an alist. Keys are properties, as upcased
strings."
(org-with-point-at pom
(when (and (derived-mode-p 'org-mode)
(ignore-errors (org-back-to-heading t)))
(org-back-to-heading-or-point-min t))
(catch 'exit
(let* ((beg (point))
(specific (and (stringp which) (upcase which)))
@ -13236,13 +13284,13 @@ unless LITERAL-NIL is non-nil."
;; Return final values.
(and (not (equal value '(nil))) (nreverse value))))))
(defun org--property-global-value (property literal-nil)
"Return value for PROPERTY in current buffer.
(defun org--property-global-or-keyword-value (property literal-nil)
"Return value for PROPERTY as defined by global properties or by keyword.
Return value is a string. Return nil if property is not set
globally. Also return nil when PROPERTY is set to \"nil\",
unless LITERAL-NIL is non-nil."
globally or by keyword. Also return nil when PROPERTY is set to
\"nil\", unless LITERAL-NIL is non-nil."
(let ((global
(cdr (or (assoc-string property org-file-properties t)
(cdr (or (assoc-string property org-keyword-properties t)
(assoc-string property org-global-properties t)
(assoc-string property org-global-properties-fixed t)))))
(if literal-nil global (org-not-nil global))))
@ -13391,12 +13439,12 @@ However, if LITERAL-NIL is set, return the string value \"nil\" instead."
value)))
(cond
((car v)
(org-back-to-heading t)
(org-back-to-heading-or-point-min t)
(move-marker org-entry-property-inherited-from (point))
(throw 'exit nil))
((org-up-heading-safe))
((org-up-heading-or-point-min))
(t
(let ((global (org--property-global-value property literal-nil)))
(let ((global (org--property-global-or-keyword-value property literal-nil)))
(cond ((not global))
(value (setq value (concat global " " value)))
(t (setq value global))))
@ -13428,8 +13476,8 @@ decreases scheduled or deadline date by one day."
(user-error "Invalid property name: \"%s\"" property)))
(org-with-point-at pom
(if (or (not (featurep 'org-inlinetask)) (org-inlinetask-in-task-p))
(org-back-to-heading t)
(org-with-limited-levels (org-back-to-heading t)))
(org-back-to-heading-or-point-min t)
(org-with-limited-levels (org-back-to-heading-or-point-min t)))
(let ((beg (point)))
(cond
((equal property "TODO")
@ -13565,19 +13613,26 @@ COLUMN formats in the current buffer."
Do nothing if the drawer already exists. The newly created
drawer is immediately hidden."
(org-with-wide-buffer
;; Set point to the position where the drawer should be inserted.
(if (or (not (featurep 'org-inlinetask)) (org-inlinetask-in-task-p))
(org-back-to-heading t)
(org-with-limited-levels (org-back-to-heading t)))
(forward-line)
(when (looking-at-p org-planning-line-re) (forward-line))
(org-back-to-heading-or-point-min t)
(org-with-limited-levels (org-back-to-heading-or-point-min t)))
(if (org-before-first-heading-p)
(while (and (org-at-comment-p) (bolp)) (forward-line))
(progn
(forward-line)
(when (looking-at-p org-planning-line-re) (forward-line))))
(unless (looking-at-p org-property-drawer-re)
;; Make sure we start editing a line from current entry, not from
;; next one. It prevents extending text properties or overlays
;; belonging to the latter.
(when (bolp) (backward-char))
(let ((begin (1+ (point)))
(when (and (bolp) (> (point) (point-min))) (backward-char))
(let ((begin (if (= (point) (point-min))
(point)
(1+ (point))))
(inhibit-read-only t))
(insert "\n:PROPERTIES:\n:END:")
(unless (= begin (point-min)) (insert "\n"))
(insert ":PROPERTIES:\n:END:")
(org-flag-drawer t nil (line-end-position 0) (point))
(when (eobp) (insert "\n"))
(org-indent-region begin (point))))))
@ -20482,6 +20537,15 @@ interactive command with similar behavior."
(error (error "Before first headline at position %d in buffer %s"
(point) (current-buffer)))))
(defun org-back-to-heading-or-point-min (&optional invisible-ok)
"Go back to heading or first point in buffer.
If point is before first heading go to first point in buffer
instead of back to heading."
(condition-case nil
(outline-back-to-heading invisible-ok)
(error
(goto-char (point-min)))))
(defun org-before-first-heading-p ()
"Before first heading?"
(org-with-limited-levels
@ -20515,6 +20579,12 @@ unless optional argument NO-INHERITANCE is non-nil."
(beginning-of-line)
(looking-at "^[ \t]*# "))))
(defun org-at-keyword-p nil
"Return t if cursor is at a keyword-line."
(save-excursion
(move-beginning-of-line 1)
(looking-at org-keyword-regexp)))
(defun org-at-drawer-p nil
"Return t if cursor is at a drawer keyword."
(save-excursion
@ -20562,6 +20632,17 @@ make a significant difference in outlines with very many siblings."
(re-search-backward (format "^\\*\\{1,%d\\} " level-up) nil t)
(funcall outline-level)))))
(defun org-up-heading-or-point-min ()
"Move to the heading line of which the present is a subheading, or point-min.
This version is needed to make point-min behave like a virtual
heading of level 0 for property-inheritance. It will return the
level of the headline found (down to 0) or nil if already at a
point before the first headline or at point-min."
(when (ignore-errors (org-back-to-heading t))
(if (< 1 (funcall outline-level))
(org-up-heading-safe)
(unless (= (point) (point-min)) (goto-char (point-min))))))
(defun org-first-sibling-p ()
"Is this heading the first child of its parents?"
(interactive)
@ -20662,28 +20743,31 @@ If there is no such heading, return nil."
(defun org-end-of-subtree (&optional invisible-ok to-heading)
"Goto to the end of a subtree."
;; This contains an exact copy of the original function, but it uses
;; `org-back-to-heading', to make it work also in invisible
;; trees. And is uses an invisible-ok argument.
;; `org-back-to-heading-or-point-min', to make it work also in invisible
;; trees and before first headline. And is uses an invisible-ok argument.
;; Under Emacs this is not needed, but the old outline.el needs this fix.
;; Furthermore, when used inside Org, finding the end of a large subtree
;; with many children and grandchildren etc, this can be much faster
;; than the outline version.
(org-back-to-heading invisible-ok)
(org-back-to-heading-or-point-min invisible-ok)
(let ((first t)
(level (funcall outline-level)))
(if (and (derived-mode-p 'org-mode) (< level 1000))
;; A true heading (not a plain list item), in Org
;; This means we can easily find the end by looking
;; only for the right number of stars. Using a regexp to do
;; this is so much faster than using a Lisp loop.
(let ((re (concat "^\\*\\{1," (int-to-string level) "\\} ")))
(forward-char 1)
(and (re-search-forward re nil 'move) (beginning-of-line 1)))
;; something else, do it the slow way
(while (and (not (eobp))
(or first (> (funcall outline-level) level)))
(setq first nil)
(outline-next-heading)))
(cond ((= level 0)
(goto-char (point-max)))
((and (derived-mode-p 'org-mode) (< level 1000))
;; A true heading (not a plain list item), in Org
;; This means we can easily find the end by looking
;; only for the right number of stars. Using a regexp to do
;; this is so much faster than using a Lisp loop.
(let ((re (concat "^\\*\\{1," (int-to-string level) "\\} ")))
(forward-char 1)
(and (re-search-forward re nil 'move) (beginning-of-line 1))))
(t
;; something else, do it the slow way
(while (and (not (eobp))
(or first (> (funcall outline-level) level)))
(setq first nil)
(outline-next-heading))))
(unless to-heading
(when (memq (preceding-char) '(?\n ?\^M))
;; Go to end of line before heading

View File

@ -1,5 +1,7 @@
#+property: header-args :var foo=1
#+property: header-args+ :var bar=2
:PROPERTIES:
:header-args: :var foo=1
:header-args+: :var bar=2
:END:
#+begin_src emacs-lisp
(+ foo bar)

View File

@ -1925,6 +1925,15 @@ e^{i\\pi}+1=0
(let ((element (org-element-at-point)))
(list (org-element-property :key element)
(org-element-property :value element))))))
;; The insides of property blocks on document level are parsed the
;; same way as headline property blocks. I.e. the concept of
;; `node-property' apply also for properties in those blocks.
(should
(equal '("abc" "value")
(org-test-with-temp-text ":PROPERTIES:\n<point>:abc: value\n:END:"
(let ((element (org-element-at-point)))
(list (org-element-property :key element)
(org-element-property :value element))))))
;; Value should be trimmed.
(should
(equal "value"
@ -2111,6 +2120,18 @@ Outside list"
(org-test-with-temp-text
"* H\nDEADLINE: <2014-03-04 tue.>\n<point>:PROPERTIES:\n:prop: value\n:END:"
(org-element-type (org-element-at-point)))))
(should
(eq 'property-drawer
(org-test-with-temp-text "<point>:PROPERTIES:\n:prop: value\n:END:"
(org-element-type (org-element-at-point)))))
(should
(eq 'property-drawer
(org-test-with-temp-text "# C\n# C\n<point>:PROPERTIES:\n:prop: value\n:END:"
(org-element-type (org-element-at-point)))))
(should-not
(eq 'property-drawer
(org-test-with-temp-text "\n<point>:PROPERTIES:\n:prop: value\n:END:"
(org-element-type (org-element-at-point)))))
;; Allow properties without value and no property at all.
(should
(eq 'property-drawer

View File

@ -386,10 +386,112 @@
;;; Drawers
(ert-deftest test-org/at-property-p ()
"Test `org-at-property-p' specifications."
(should
(equal 't
(org-test-with-temp-text "* H\n:PROPERTIES:\n<point>:PROP: t\n:END:\n"
(org-at-property-p))))
(should
(equal 't
(org-test-with-temp-text ":PROPERTIES:\n<point>:PROP: t\n:END:\n"
(org-at-property-p)))))
(ert-deftest test-org/at-property-block-p ()
"Test `org-at-property-block-p' specifications."
(should
(equal 't
(org-test-with-temp-text "* H\n<point>:PROPERTIES:\n:PROP: t\n:END:\n"
(org-at-property-block-p))))
(should
(equal 't
(org-test-with-temp-text ":PROPERTIES:\n:PROP: t\n:END:\n"
(org-at-property-block-p))))
;; The function only returns t if point is at the first line of a
;; property block.
(should-not
(equal 't
(org-test-with-temp-text ":PROPERTIES:\n<point>:PROP: t\n:END:\n"
(org-at-property-block-p)))))
(ert-deftest test-org/get-property-block ()
"Test `org-get-property-block' specifications."
(should
(equal '(14 . 14)
(org-test-with-temp-text ":PROPERTIES:\n:END:\n* H\n"
(org-get-property-block))))
(should
(equal '(14 . 14)
(org-test-with-temp-text ":PROPERTIES:\n:END:\n"
(org-get-property-block))))
;; Comments above a document property block is ok.
(should
(equal '(18 . 18)
(org-test-with-temp-text "# C\n:PROPERTIES:\n:END:\n"
(org-get-property-block))))
;; Keywords above a document property block is ok.
(should
(equal '(22 . 22)
(org-test-with-temp-text "# C\n# C\n:PROPERTIES:\n:END:\n"
(org-get-property-block))))
;; Comments and keywords are allowed before a document property block.
(should
(equal '(18 . 27)
(org-test-with-temp-text "# C\n:PROPERTIES:\n:KEY: V:\n:END:\n"
(org-get-property-block))))
;; A document property block will not be valid if there are lines
;; with whitespace above it
(should-not
(org-test-with-temp-text "\n:PROPERTIES:\n:END:\n"
(org-get-property-block)))
(should
(equal '(18 . 18)
(org-test-with-temp-text "* H\n:PROPERTIES:\n:END:\n<point>"
(org-get-property-block))))
(should
(equal "* H\n:PROPERTIES:\n:END:\n"
(org-test-with-temp-text "* H"
(let ((org-adapt-indentation nil))
(org-get-property-block nil 'force))
(buffer-string))))
(should
(equal ":PROPERTIES:\n:END:\n"
(org-test-with-temp-text ""
(org-get-property-block nil 'force)
(buffer-string))))
(should
(equal "* H1\n :PROPERTIES:\n :END:\n* H2"
(org-test-with-temp-text "* H1\n* H2"
(let ((org-adapt-indentation t))
(org-get-property-block nil 'force))
(buffer-string)))))
(ert-deftest test-org/insert-property-drawer ()
"Test `org-insert-property-drawer' specifications."
;; Error before first headline.
(should-error (org-test-with-temp-text "" (org-insert-property-drawer)))
;; Insert drawer in empty buffer
(should
(equal ":PROPERTIES:\n:END:\n"
(org-test-with-temp-text ""
(let ((org-adapt-indentation nil)) (org-insert-property-drawer))
(buffer-string))))
;; Insert drawer in document header with existing comment and
;; keyword.
(should
(equal "# C\n:PROPERTIES:\n:END:\n#+TITLE: T"
(org-test-with-temp-text "# C\n#+TITLE: T"
(let ((org-adapt-indentation nil)) (org-insert-property-drawer))
(buffer-string))))
(should
(equal ":PROPERTIES:\n:END:"
(org-test-with-temp-text ":PROPERTIES:\n:END:"
(let ((org-adapt-indentation nil)) (org-insert-property-drawer))
(buffer-string))))
;; Insert drawer in document header with one existing heading in buffer.
(should
(equal ":PROPERTIES:\n:END:\n* T\n"
(org-test-with-temp-text "<point>\n* T\n"
(let ((org-adapt-indentation nil)) (org-insert-property-drawer))
(buffer-string))))
;; Insert drawer right after headline if there is no planning line,
;; or after it otherwise.
(should
@ -2178,19 +2280,19 @@ SCHEDULED: <2014-03-04 tue.>"
(equal "foo=1"
(org-test-with-temp-text "#+PROPERTY: var foo=1"
(org-mode-restart)
(cdr (assoc "var" org-file-properties)))))
(cdr (assoc "var" org-keyword-properties)))))
(should
(equal
"foo=1 bar=2"
(org-test-with-temp-text "#+PROPERTY: var foo=1\n#+PROPERTY: var+ bar=2"
(org-mode-restart)
(cdr (assoc "var" org-file-properties)))))
(cdr (assoc "var" org-keyword-properties)))))
(should
(equal
"foo=1 bar=2"
(org-test-with-temp-text "#+PROPERTY: var foo=1\n#+PROPERTY: VAR+ bar=2"
(org-mode-restart)
(cdr (assoc "var" org-file-properties)))))
(cdr (assoc "var" org-keyword-properties)))))
;; ARCHIVE keyword.
(should
(equal "%s_done::"
@ -2207,7 +2309,7 @@ SCHEDULED: <2014-03-04 tue.>"
(equal "test"
(org-test-with-temp-text "#+CATEGORY: test"
(org-mode-restart)
(cdr (assoc "CATEGORY" org-file-properties)))))
(cdr (assoc "CATEGORY" org-keyword-properties)))))
;; COLUMNS keyword.
(should
(equal "%25ITEM %TAGS %PRIORITY %TODO"
@ -2291,7 +2393,7 @@ SCHEDULED: <2014-03-04 tue.>"
(org-test-with-temp-text
(format "#+SETUPFILE: \"%s/examples/setupfile.org\"" org-test-dir)
(org-mode-restart)
(cdr (assoc "a" org-file-properties))))))
(cdr (assoc "a" org-keyword-properties))))))
@ -4941,6 +5043,66 @@ Paragraph<point>"
(org-test-with-temp-text "* H\n:PROPERTIES:\n:A: 1\n:A+: 2\n:END:"
(org-property-values "A")))))
(ert-deftest test-org/set-property ()
"Test `org-set-property' specifications."
(should
(equal
":PROPERTIES:\n:TEST: t\n:END:\n"
(org-test-with-temp-text ""
(let ((org-property-format "%s %s"))
(org-set-property "TEST" "t"))
(buffer-string))))
(should
(equal
"* H\n:PROPERTIES:\n:TEST: t\n:END:\n"
(org-test-with-temp-text "* H"
(let ((org-adapt-indentation nil)
(org-property-format "%s %s"))
(org-set-property "TEST" "t"))
(buffer-string)))))
(ert-deftest test-org/delete-property ()
"Test `org-delete-property' specifications."
(should
(equal
""
(org-test-with-temp-text ":PROPERTIES:\n:TEST: t\n:END:\n"
(org-delete-property "TEST")
(buffer-string))))
(should
(equal
":PROPERTIES:\n:TEST1: t\n:END:\n"
(org-test-with-temp-text ":PROPERTIES:\n:TEST1: t\n:TEST2: t\n:END:\n"
(org-delete-property "TEST2")
(buffer-string))))
(should
(equal
"* H\n"
(org-test-with-temp-text "* H\n:PROPERTIES:\n:TEST: t\n:END:\n"
(org-delete-property "TEST")
(buffer-string))))
(should
(equal
"* H\n:PROPERTIES:\n:TEST1: t\n:END:\n"
(org-test-with-temp-text "* H\n:PROPERTIES:\n:TEST1: t\n:TEST2: t\n:END:\n"
(org-delete-property "TEST2")
(buffer-string)))))
(ert-deftest test-org/delete-property-globally ()
"Test `org-delete-property-global' specifications."
(should
(equal
""
(org-test-with-temp-text ":PROPERTIES:\n:TEST: t\n:END:\n"
(org-delete-property-globally "TEST")
(buffer-string))))
(should
(equal
"* H\n"
(org-test-with-temp-text ":PROPERTIES:\n:TEST: t\n:END:\n* H\n:PROPERTIES:\n:TEST: nil\n:END:"
(org-delete-property-globally "TEST")
(buffer-string)))))
(ert-deftest test-org/find-property ()
"Test `org-find-property' specifications."
;; Regular test.
@ -5022,6 +5184,10 @@ Paragraph<point>"
(ert-deftest test-org/entry-get ()
"Test `org-entry-get' specifications."
;; Regular test.
(should
(equal "1"
(org-test-with-temp-text ":PROPERTIES:\n:A: 1\n:END:"
(org-entry-get (point) "A"))))
(should
(equal "1"
(org-test-with-temp-text "* H\n:PROPERTIES:\n:A: 1\n:END:"
@ -5061,6 +5227,11 @@ Paragraph<point>"
(org-entry-get (point) "B" nil t)))
;; Handle inheritance, when allowed. Include extended values and
;; possibly global values.
(should
(equal
"1"
(org-test-with-temp-text ":PROPERTIES:\n:A: 1\n:END:\n* H"
(org-entry-get (point-max) "A" t))))
(should
(equal
"1"
@ -5076,12 +5247,30 @@ Paragraph<point>"
(org-test-with-temp-text "* H\n:PROPERTIES:\n:A: 1\n:END:\n** H2"
(let ((org-use-property-inheritance nil))
(org-entry-get (point-max) "A" 'selective))))
(should
(equal
"1 2"
(org-test-with-temp-text
":PROPERTIES:\n:A: 1\n:END:\n* H\n:PROPERTIES:\n:A+: 2\n:END:"
(org-entry-get (point-max) "A" t))))
(should
(equal
"1 2"
(org-test-with-temp-text
"* H\n:PROPERTIES:\n:A: 1\n:END:\n** H2\n:PROPERTIES:\n:A+: 2\n:END:"
(org-entry-get (point-max) "A" t))))
(should
(equal
"1 2"
(org-test-with-temp-text
":PROPERTIES:\n:A: 1\n:END:\n* H1\n* H2\n:PROPERTIES:\n:A+: 2\n:END:"
(org-entry-get (point-max) "A" t))))
(should
(equal
"1 2"
(org-test-with-temp-text
"* H1\n:PROPERTIES:\n:A: 1\n:END:\n* H2.1\n* H2.2\n:PROPERTIES:\n:A+: 2\n:END:"
(org-entry-get (point-max) "A" t))))
(should
(equal "1"
(org-test-with-temp-text
@ -5093,6 +5282,14 @@ Paragraph<point>"
(org-test-with-temp-text
"#+PROPERTY: A 0\n* H\n:PROPERTIES:\n:A+: 1\n:END:"
(org-mode-restart)
(org-entry-get (point-max) "A" t))))
;; document level property-drawer has precedance over
;; global-property by PROPERTY-keyword.
(should
(equal "0 2"
(org-test-with-temp-text
":PROPERTIES:\n:A: 0\n:END:\n#+PROPERTY: A 1\n* H\n:PROPERTIES:\n:A+: 2\n:END:"
(org-mode-restart)
(org-entry-get (point-max) "A" t)))))
(ert-deftest test-org/entry-properties ()
@ -5416,8 +5613,44 @@ Paragraph<point>"
(let ((org-use-property-inheritance t))
(org-refresh-properties "A" 'org-test))
(get-text-property (point) 'org-test))))
;; When a document level property-drawer is used, those properties
;; should work exactly like headline-properties as if at a
;; headline-level 0.
(should
(equal "1"
(org-test-with-temp-text
":PROPERTIES:\n:A: 1\n:END:\n"
(org-mode-restart)
(let ((org-use-property-inheritance t))
(org-refresh-properties "A" 'org-test))
(get-text-property (point) 'org-test))))
(should-not
(equal "1"
(org-test-with-temp-text
":PROPERTIES:\n:A: 1\n:END:\n<point>* H1"
(org-mode-restart)
(let ((org-use-property-inheritance nil))
(org-refresh-properties "A" 'org-test))
(get-text-property (point) 'org-test))))
(should
(equal "1"
(org-test-with-temp-text
":PROPERTIES:\n:A: 1\n:END:\n<point>* H1"
(org-mode-restart)
(let ((org-use-property-inheritance t))
(org-refresh-properties "A" 'org-test))
(get-text-property (point) 'org-test))))
(should
(equal "2"
(org-test-with-temp-text
":PROPERTIES:\n:A: 1\n:END:\n<point>* H1\n:PROPERTIES:\n:A: 2\n:END:"
(org-mode-restart)
(let ((org-use-property-inheritance t))
(org-refresh-properties "A" 'org-test))
(get-text-property (point) 'org-test))))
;; When property is inherited, use global value across the whole
;; buffer. However local values have precedence.
;; buffer. However local values have precedence, as well as the
;; document level property-drawer.
(should-not
(equal "1"
(org-test-with-temp-text "#+PROPERTY: A 1\n<point>* H1"
@ -5437,10 +5670,62 @@ Paragraph<point>"
(org-test-with-temp-text
"#+PROPERTY: A 1\n<point>* H\n:PROPERTIES:\n:A: 2\n:END:"
(org-mode-restart)
(let ((org-use-property-inheritance t))
(org-refresh-properties "A" 'org-test))
(get-text-property (point) 'org-test))))
;; When both keyword-property and document-level property-block is
;; defined, the property-block has precedance.
(should
(equal "1"
(org-test-with-temp-text
":PROPERTIES:\n:A: 1\n:END:\n#+PROPERTY: A 2\n<point>* H1"
(org-mode-restart)
(let ((org-use-property-inheritance t))
(org-refresh-properties "A" 'org-test))
(get-text-property (point) 'org-test)))))
(ert-deftest test-org/refresh-category-properties ()
"Test `org-refresh-category-properties' specifications"
(should
(equal "cat1"
(org-test-with-temp-text
":PROPERTIES:\n:CATEGORY: cat1\n:END:"
(org-refresh-category-properties)
(get-text-property (point) 'org-category))))
(should
(equal "cat1"
(org-test-with-temp-text
"* H\n:PROPERTIES:\n:CATEGORY: cat1\n:END:"
(org-refresh-category-properties)
(get-text-property (point) 'org-category))))
;; Even though property-inheritance is deactivated, category
;; property should be inherited. As described in
;; `org-use-property-inheritance'.
(should
(equal "cat1"
(org-test-with-temp-text
":PROPERTIES:\n:CATEGORY: cat1\n:END:\n<point>* H"
(org-mode-restart)
(let ((org-use-property-inheritance nil))
(org-refresh-category-properties))
(get-text-property (point) 'org-category))))
(should
(equal "cat1"
(org-test-with-temp-text
":PROPERTIES:\n:CATEGORY: cat1\n:END:\n<point>* H"
(org-mode-restart)
(let ((org-use-property-inheritance t))
(org-refresh-category-properties))
(get-text-property (point) 'org-category))))
(should
(equal "cat2"
(org-test-with-temp-text
":PROPERTIES:\n:CATEGORY: cat1\n:END:\n<point>* H\n:PROPERTIES:\n:CATEGORY: cat2\n:END:\n"
(org-mode-restart)
(let ((org-use-property-inheritance t))
(org-refresh-category-properties))
(get-text-property (point) 'org-category)))))
;;; Refile