Explorar el Código

Add Org Num minor mode

* doc/org-manual.org (Headlines): Refer to new section.
(Dynamic Headline Numbering): New section.
* lisp/org-num.el:
* testing/lisp/test-org-num.el: New files.
Nicolas Goaziou hace 6 años
padre
commit
69c300bbf6
Se han modificado 4 ficheros con 755 adiciones y 0 borrados
  1. 33 0
      doc/org-manual.org
  2. 6 0
      etc/ORG-NEWS
  3. 455 0
      lisp/org-num.el
  4. 261 0
      testing/lisp/test-org-num.el

+ 33 - 0
doc/org-manual.org

@@ -427,6 +427,9 @@ Some people find the many stars too noisy and would prefer an outline
 that has whitespace followed by a single star as headline starters.
 See [[*A Cleaner Outline View]].
 
+Headline are not numbered.  However, you may want to dynamically
+number some, or all, of them.  See [[*Dynamic Headline Numbering]].
+
 #+vindex: org-cycle-separator-lines
 An empty line after the end of a subtree is considered part of it and
 is hidden when the subtree is folded.  However, if you leave at least
@@ -19025,6 +19028,36 @@ headings as shown in examples below.
      org-convert-to-odd-levels)}}} and {{{kbd(M-x
      org-convert-to-oddeven-levels)}}}.
 
+** Dynamic Headline Numbering
+:PROPERTIES:
+:DESCRIPTION: Display and update outline numbering.
+:END:
+
+#+cindex: Org Num mode
+#+cindex: number headlines
+The Org Num minor mode, toggled with {{{kbd(M-x org-num-mode)}}},
+displays on top of headlines.  It also updates numbering automatically
+upon changes to the structure of the document.
+
+#+vindex: org-num-max-level
+#+vindex: org-num-skip-tags
+#+vindex: org-num-skip-commented
+#+vindex: org-num-skip-unnumbered
+By default, all headlines are numbered.  You can limit numbering to
+specific headlines according to their level, tags, =COMMENT= keyword,
+or =UNNUMBERED= property.  Set ~org-num-max-level~,
+~org-num-skip-tags~, ~org-num-skip-commented~,
+~org-num-skip-unnumbered~, or ~org-num-skip-footnotes~ accordingly.
+
+#+vindex: org-num-skip-footnotes
+If ~org-num-skip-footnotes~ is non-~nil~, footnotes sections (see
+[[*Creating Footnotes]]) are not numbered either.
+
+#+vindex: org-num-face
+#+vindex: org-num-format-function
+You can control how the numbering is displayed by setting
+~org-num-face~ and ~org-num-format-function~.
+
 ** Using Org on a TTY
 :PROPERTIES:
 :DESCRIPTION: Using Org on a tty.

+ 6 - 0
etc/ORG-NEWS

@@ -42,6 +42,12 @@ See [[git:3367ac9457]] for details.
 ** New features
 *** Babel
 **** Add LaTeX output support in PlantUML
+*** New minor mode to display headline numbering
+
+Use =<M-x org-num-mode>= to get a visual indication of the numbering
+in the outline.  The numbering is also automatically updated upon
+changes in the buffer.
+
 *** New property =HTML_HEADLINE_CLASS= in HTML export
 The new property =HTML_HEADLINE_CLASS= assigns a class attribute to
 a headline.

+ 455 - 0
lisp/org-num.el

@@ -0,0 +1,455 @@
+;;; org-num.el --- Dynamic Headlines Numbering  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2018-2019  Free Software Foundation, Inc.
+
+;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
+;; Keywords: outlines, hypermedia, calendar, wp
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides dynamic numbering for Org headlines.  Use
+;;
+;;     <M-x org-num-mode>
+;;
+;; to toggle it.
+;;
+;; You can select what is numbered according to level, tags, COMMENT
+;; keyword, or UNNUMBERED property. You can also skip footnotes
+;; sections. See `org-num-max-level', `org-num-skip-tags',
+;; `org-num-skip-commented', `org-num-skip-unnumbered', and
+;; `org-num-skip-footnotes' for details.
+;;
+;; You can also control how the numbering is displayed by setting
+;;`org-num-face' and `org-num-format-function'.
+;;
+;; Internally, the library handles an ordered list, per buffer
+;; position, of overlays in `org-num--overlays'.  These overlays are
+;; marked with the `org-num' property set to a non-nil value.
+;;
+;; Overlays store the level of the headline in the `level' property,
+;; and the face used for the numbering in `numbering-face'.
+;;
+;; The `skip' property is set to t when the corresponding headline has
+;; some characteristic -- e.g., a node property, or a tag -- that
+;; prevents it from being numbered.
+;;
+;; An overlay with `org-num' property set to `invalid' is called an
+;; invalid overlay.  Modified overlays automatically become invalid
+;; and set `org-num--invalid-flag' to a non-nil value.  After
+;; a change, `org-num--invalid-flag' indicates numbering needs to be
+;; updated and invalid overlays indicate where the buffer needs to be
+;; parsed.  So does `org-num--missing-overlay' variable.  See
+;; `org-num--verify' function for details.
+;;
+;; Numbering display is done through the `after-string' property.
+
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'org-macs)
+
+(defvar org-comment-string)
+(defvar org-complex-heading-regexp)
+(defvar org-cycle-level-faces)
+(defvar org-n-level-faces)
+(defvar org-odd-levels-only)
+(defvar org-level-faces)
+
+(declare-function org-back-to-heading "org" (&optional invisible-ok))
+(declare-function org-entry-get "org" (pom property &optional inherit literal-nil))
+(declare-function org-reduced-level "org" (l))
+
+
+;;; Customization
+
+(defcustom org-num-face nil
+  "Face to use for numbering.
+When nil, use the same face as the headline.  This value is
+ignored if `org-num-format-function' specifies a face for its
+output."
+  :group 'org-appearance
+  :type '(choice (const :tag "Like the headline" nil)
+                 (face :tag "Use face"))
+  :safe (lambda (val) (or (null val) (facep val))))
+
+(defcustom org-num-format-function 'org-num-default-format
+  "Function used to display numbering.
+It is called with one argument, a list of numbers, and should
+return a string, or nil.  When nil, no numbering is displayed.
+Any `face' text property on the returned string overrides
+`org-num-face'."
+  :group 'org-appearance
+  :type 'function
+  :safe nil)
+
+(defcustom org-num-max-level nil
+  "Level below which headlines are not numbered.
+When set to nil, all headlines are numbered."
+  :group 'org-appearance
+  :type '(choice (const :tag "Number everything" nil)
+                 (integer :tag "Stop numbering at level"))
+  :safe (lambda (val) (or (null val) (wholenump val))))
+
+(defcustom org-num-skip-commented nil
+  "Non-nil means commented sub-trees are not numbered."
+  :group 'org-appearance
+  :type 'boolean
+  :safe #'booleanp)
+
+(defcustom org-num-skip-footnotes nil
+  "Non-nil means footnotes sections are not numbered."
+  :group 'org-appearance
+  :type 'boolean
+  :safe #'booleanp)
+
+(defcustom org-num-skip-tags nil
+  "List of tags preventing the numbering of sub-trees.
+
+For example, add \"ARCHIVE\" to this list to avoid numbering
+archived sub-trees.
+
+Tag in this list prevent numbering the whole sub-tree,
+irrespective to `org-use-tags-inheritance', or other means to
+control tag inheritance."
+  :group 'org-appearance
+  :type '(repeat (string :tag "Tag"))
+  :safe (lambda (val) (and (listp val) (cl-every #'stringp val))))
+
+(defcustom org-num-skip-unnumbered nil
+  "Non-nil means numbering obeys to UNNUMBERED property."
+  :group 'org-appearance
+  :type 'boolean
+  :safe #'booleanp)
+
+
+;;; Internal Variables
+
+(defconst org-num--comment-re (format "\\`%s\\(?: \\|$\\)" org-comment-string)
+  "Regexp matching a COMMENT keyword at headline beginning.")
+
+(defvar-local org-num--overlays nil
+  "Ordered list of overlays used for numbering outlines.")
+
+(defvar-local org-num--skip-level nil
+  "Level below which headlines from current tree are not numbered.
+When nil, all headlines are numbered.  It is used to handle
+inheritance of no-numbering attributes.")
+
+(defvar-local org-num--numbering nil
+  "Current headline numbering.
+A numbering is a list of integers, in reverse order.  So numbering
+for headline \"1.2.3\" is (3 2 1).")
+
+(defvar-local org-num--missing-overlay nil
+  "Buffer position signaling a headline without an overlay.")
+
+(defvar-local org-num--invalid-flag nil
+  "Non-nil means an overlay became invalid since last update.")
+
+
+;;; Internal Functions
+
+(defsubst org-num--headline-regexp ()
+  "Return regexp matching a numbered headline."
+  (if (null org-num-max-level) (org-with-limited-levels org-outline-regexp-bol)
+    (format "^\\*\\{1,%d\\} "
+            (if org-odd-levels-only (1- (* 2 org-num-max-level))
+              org-num-max-level))))
+
+(defsubst org-num--overlay-p (o)
+  "Non-nil if overlay O is a numbering overlay."
+  (overlay-get o 'org-num))
+
+(defsubst org-num--valid-overlay-p (o)
+  "Non-nil if overlay O is still active in the buffer."
+  (not (eq 'invalid (overlay-get o 'org-num))))
+
+(defsubst org-num--invalidate-overlay (o)
+  "Mark overlay O as invalid.
+Update `org-num--invalid-flag' accordingly."
+  (overlay-put o 'org-num 'invalid)
+  (setq org-num--invalid-flag t))
+
+(defun org-num--make-overlay (numbering level skip)
+  "Return overlay for numbering headline at point.
+
+NUMBERING is the numbering to use, as a list of integers, or nil
+if nothing should be displayed.  LEVEL is the level of the
+headline.  SKIP is its skip value.
+
+Assume point is at a headline."
+  (let ((after-edit-functions
+         (list (lambda (o &rest _) (org-num--invalidate-overlay o))))
+        (o (save-excursion
+             (beginning-of-line)
+             (skip-chars-forward "*")
+             (make-overlay (line-beginning-position) (1+ (point))))))
+    (overlay-put o 'org-num t)
+    (overlay-put o 'skip skip)
+    (overlay-put o 'level level)
+    (overlay-put o 'numbering-face
+                 (or org-num-face
+                     ;; Compute face that would be used at the
+                     ;; headline.  We cannot extract it from the
+                     ;; buffer: at the time the overlay is created,
+                     ;; Font Lock has not proceeded yet.
+                     (nth (if org-cycle-level-faces
+                              (% (1- level) org-n-level-faces)
+                            (1- (min level org-n-level-faces)))
+                          org-level-faces)))
+    (overlay-put o 'modification-hooks after-edit-functions)
+    (overlay-put o 'insert-in-front-hooks after-edit-functions)
+    (org-num--refresh-display o numbering)
+    o))
+
+(defun org-num--refresh-display (overlay numbering)
+  "Refresh OVERLAY's display.
+NUMBERING specifies the new numbering, as a list of integers, or
+nil if nothing should be displayed.  Assume OVERLAY is valid."
+  (let ((display (and numbering
+                      (funcall org-num-format-function (reverse numbering)))))
+    (when (and display (not (get-text-property 0 'face display)))
+      (org-add-props display `(face ,(overlay-get overlay 'numbering-face))))
+    (overlay-put overlay 'after-string display)))
+
+(defun org-num--skip-value ()
+  "Return skip value for headline at point.
+Value is t when headline should not be numbered, and nil
+otherwise."
+  (org-match-line org-complex-heading-regexp)
+  (let ((title (match-string 4))
+        (tags (and org-num-skip-tags
+                   (match-end 5)
+                   (org-split-string (match-string 5) ":"))))
+    (or (and org-num-skip-footnotes
+             org-footnote-section
+             (equal title org-footnote-section))
+        (and org-num-skip-commented
+             (let ((case-fold-search nil))
+               (string-match org-num--comment-re title))
+             t)
+        (and org-num-skip-tags
+             (cl-some (lambda (tag) (member tag org-num-skip-tags))
+                      tags)
+             t)
+        (and org-num-skip-unnumbered
+             (org-entry-get (point) "UNNUMBERED")
+             t))))
+
+(defun org-num--current-numbering (level skip)
+  "Return numbering for current headline.
+LEVEL is headline's level, and SKIP its skip value.  Return nil
+if headline should be skipped."
+  (cond
+   ;; Skipped by inheritance.
+   ((and org-num--skip-level (> level org-num--skip-level)) nil)
+   ;; Skipped by a non-nil skip value; set `org-num--skip-level'
+   ;; to skip the whole sub-tree later on.
+   (skip (setq org-num--skip-level level) nil)
+   (t
+    (setq org-num--skip-level nil)
+    ;; Compute next numbering, and update `org-num--numbering'.
+    (let ((last-level (length org-num--numbering)))
+      (setq org-num--numbering
+            (cond
+             ;; First headline : nil => (1), or (1 0)...
+             ((null org-num--numbering) (cons 1 (make-list (1- level) 0)))
+             ;; Sibling: (1 1) => (2 1).
+             ((= level last-level)
+              (cons (1+ (car org-num--numbering)) (cdr org-num--numbering)))
+             ;; Parent: (1 1 1) => (2 1), or (2).
+             ((< level last-level)
+              (let ((suffix (nthcdr (- last-level level) org-num--numbering)))
+                (cons (1+ (car suffix)) (cdr suffix))))
+             ;; Child: (1 1) => (1 1 1), or (1 0 1 1)...
+             (t
+              (append (cons 1 (make-list (- level last-level 1) 0))
+                      org-num--numbering))))))))
+
+(defun org-num--number-region (start end)
+  "Add numbering overlays between START and END positions.
+When START or END are nil, use buffer boundaries.  Narrowing, if
+any, is ignored.  Return the list of created overlays, newest
+first."
+  (org-with-point-at (or start 1)
+    ;; Do not match headline starting at START.
+    (when start (end-of-line))
+    (let ((regexp (org-num--headline-regexp))
+          (new nil))
+      (while (re-search-forward regexp end t)
+        (let* ((level (org-reduced-level
+                       (- (match-end 0) (match-beginning 0) 1)))
+               (skip (org-num--skip-value))
+               (numbering (org-num--current-numbering level skip)))
+          ;; Apply numbering to current headline.  Store overlay for
+          ;; the return value.
+          (push (org-num--make-overlay numbering level skip)
+                new)))
+      new)))
+
+(defun org-num--update ()
+  "Update buffer's numbering.
+This function removes invalid overlays and refreshes numbering
+for the valid ones in the numbering overlays list.  It also adds
+missing overlays to that list."
+  (setq org-num--skip-level nil)
+  (setq org-num--numbering nil)
+  (let ((new-overlays nil)
+        (overlay nil))
+    (while (setq overlay (pop org-num--overlays))
+      (cond
+       ;; Valid overlay.
+       ;;
+       ;; First handle possible missing overlays OVERLAY.  If missing
+       ;; overlay marker is pointing before next overlay and after the
+       ;; last known overlay, make sure to parse the buffer between
+       ;; these two overlays.
+       ((org-num--valid-overlay-p overlay)
+        (let ((next (overlay-start overlay))
+              (last (and new-overlays (overlay-start (car new-overlays)))))
+          (cond
+           ((null org-num--missing-overlay))
+           ((> org-num--missing-overlay next))
+           ((or (null last) (> org-num--missing-overlay last))
+            (setq org-num--missing-overlay nil)
+            (setq new-overlays (nconc (org-num--number-region last next)
+                                      new-overlays)))
+           ;; If it is already after the last known overlay, reset it:
+           ;; some previous invalid overlay already triggered the
+           ;; necessary parsing.
+           (t
+            (setq org-num--missing-overlay nil))))
+        ;; Update OVERLAY's numbering.
+        (let* ((level (overlay-get overlay 'level))
+               (skip (overlay-get overlay 'skip))
+               (numbering (org-num--current-numbering level skip)))
+          (org-num--refresh-display overlay numbering)
+          (push overlay new-overlays)))
+       ;; Invalid overlay.  It indicates that the buffer needs to be
+       ;; parsed again between the two surrounding valid overlays or
+       ;; buffer boundaries.
+       (t
+        ;; Delete all consecutive invalid overlays: we re-create all
+        ;; overlays between last valid overlay and the next one.
+        (delete-overlay overlay)
+        (while (and org-num--overlays
+                    (not (org-num--valid-overlay-p (car org-num--overlays))))
+          (delete-overlay (pop org-num--overlays)))
+        ;; Create and register new overlays.
+        (let ((last (and new-overlays (overlay-start (car new-overlays))))
+              (next (and org-num--overlays
+                         (overlay-start (car org-num--overlays)))))
+          (setq new-overlays (nconc (org-num--number-region last next)
+                                    new-overlays))))))
+    ;; If invalid position hasn't been handled yet, it must be located
+    ;; between last valid overlay and end of the buffer.  Parse that
+    ;; area before returning.
+    (when org-num--missing-overlay
+      (let ((last (and new-overlays (overlay-start (car new-overlays)))))
+        (setq new-overlays (nconc (org-num--number-region last nil)
+                                  new-overlays))))
+    ;; Numbering is now up-to-date.  Reset invalid flag.  Also return
+    ;; `org-num--overlays' in a sorted fashion.
+    (setq org-num--invalid-flag nil)
+    (setq org-num--overlays (nreverse new-overlays))))
+
+(defun org-num--verify (beg end _)
+  "Check numbering integrity; update it if necessary.
+This function is meant to be used in `after-change-functions'.
+See this variable for the meaning of BEG and END."
+  (setq org-num--missing-overlay nil)
+  (save-match-data
+    (org-with-point-at beg
+      (let ((regexp (org-num--headline-regexp)))
+        ;; At this point, directly altered overlays between BEG and
+        ;; END are marked as invalid and will trigger a full update.
+        ;; However, there are still two cases to handle.
+        ;;
+        ;; First, some valid overlays may need to be invalidated, due
+        ;; to an indirect change.  That happens when the skip value --
+        ;; see `org-num--skip-value' -- of the heading BEG belongs to
+        ;; is altered, or when deleting the newline character right
+        ;; before the next headline.
+        (save-excursion
+          ;; Bail out if we're before first headline or within
+          ;; a headline too deep to be numbered.
+          (when (and (org-with-limited-levels
+                      (ignore-errors (org-back-to-heading t)))
+                     (looking-at regexp))
+            (pcase (get-char-property-and-overlay (point) 'org-num)
+              (`(nil)
+               ;; At a headline, without a numbering overlay: change
+               ;; just created one.  Mark it for parsing.
+               (setq org-num--missing-overlay (point)))
+              (`(t . ,o)
+               ;; Check if skip value changed.  Invalidate overlay
+               ;; accordingly.
+               (unless (eq (org-num--skip-value) (overlay-get o 'skip))
+                 (org-num--invalidate-overlay o)))
+              (_ nil))))
+        ;; Deleting the newline character before a numbering overlay
+        ;; doesn't invalidate it, even though it could land in the
+        ;; middle of a line.  Be sure to catch this case.
+        (when (and (= beg end) (not (bolp)))
+          (pcase (get-char-property-and-overlay (point) 'org-num)
+            (`(t . ,o) (org-num--invalidate-overlay o))
+            (_ nil)))
+        ;; Second, if nothing is marked as invalid, and therefore if
+        ;; no full update is due so far, changes may still have
+        ;; created new headlines, at BEG -- which is actually handled
+        ;; by the previous phase --, or, in case of a multi-line
+        ;; insertion, at END, or in-between.
+        (unless (or org-num--invalid-flag
+                    org-num--missing-overlay
+                    (<= end (line-end-position))) ;single line change
+          (forward-line)
+          (when (or (re-search-forward regexp end 'move)
+                    ;; Check if change created a headline after END.
+                    (progn (skip-chars-backward "*") (looking-at regexp)))
+            (setq org-num--missing-overlay (line-beginning-position))))))
+    ;; Update numbering only if a headline was altered or created.
+    (when (or org-num--missing-overlay org-num--invalid-flag)
+      (org-num--update))))
+
+
+;;; Public Functions
+
+;;;###autoload
+(defun org-num-default-format (numbering)
+  "Default numbering display function.
+NUMBERING is a list of numbers."
+  (concat (mapconcat #'number-to-string numbering ".") " "))
+
+;;;###autoload
+(define-minor-mode org-num-mode
+  "Dynamic numbering of headlines in an Org buffer."
+  :lighter " o#"
+  (cond
+   (org-num-mode
+    (unless (derived-mode-p 'org-mode)
+      (user-error "Cannot activate headline numbering outside Org mode"))
+    (setq org-num--numbering nil)
+    (setq org-num--overlays (nreverse (org-num--number-region nil nil)))
+    (add-hook 'after-change-functions #'org-num--verify nil t))
+   (t
+    (mapc #'delete-overlay org-num--overlays)
+    (setq org-num--overlays nil)
+    (remove-hook 'after-change-functions #'org-num--verify t))))
+
+
+(provide 'org-num)
+;;; org-num.el ends here

+ 261 - 0
testing/lisp/test-org-num.el

@@ -0,0 +1,261 @@
+;;; test-org-num.el --- Tests for Org Num library    -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2018  Nicolas Goaziou
+
+;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
+
+;; This file is not part of GNU Emacs.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+;; FIXME: this test fails in batch mode.
+;;
+;; (ert-deftest test-org-num/face ()
+;;   "Test `org-num-face' parameter."
+;;   (should
+;;    (equal
+;;     '(foo)
+;;     (org-test-with-temp-text "* H1"
+;;       (let ((org-num-face 'foo)) (org-num-mode 1))
+;;       (mapcar (lambda (o)
+;; 		(get-text-property 0 'face (overlay-get o 'after-string)))
+;; 	      (overlays-in (point-min) (point-max)))))))
+
+(ert-deftest test-org-num/format-function ()
+  "Test `org-num-format-function' parameter."
+  (should
+   (equal '("foo" "foo")
+          (org-test-with-temp-text "* H1\n** H2"
+            (let ((org-num-format-function (lambda (_) "foo")))
+              (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Preserve face, when set.
+  (should
+   (equal-including-properties
+    '(#("foo" 0 3 (face bar)))
+    (org-test-with-temp-text "* H1"
+      (let ((org-num-format-function
+             (lambda (_) (org-add-props "foo" nil 'face 'bar))))
+        (org-num-mode 1))
+      (mapcar (lambda (o) (overlay-get o 'after-string))
+              (overlays-in (point-min) (point-max))))))
+  ;; Set face override `org-num-face'.
+  (should
+   (equal-including-properties
+    '(#("foo" 0 3 (face bar)))
+    (org-test-with-temp-text "* H1"
+      (let ((org-num-face 'baz)
+            (org-num-format-function
+             (lambda (_) (org-add-props "foo" nil 'face 'bar))))
+        (org-num-mode 1))
+      (mapcar (lambda (o) (overlay-get o 'after-string))
+              (overlays-in (point-min) (point-max)))))))
+
+(ert-deftest test-org-num/max-level ()
+  "Test `org-num-max-level' option."
+  (should
+   (equal '("1.1 " "1 ")
+          (org-test-with-temp-text "* H1\n** H2\n*** H3"
+            (let ((org-num-max-level 2)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max)))))))
+
+(ert-deftest test-org-num/skip-numbering ()
+  "Test various skip numbering parameters."
+  ;; Skip commented headlines.
+  (should
+   (equal '(nil "1 ")
+          (org-test-with-temp-text "* H1\n* COMMENT H2"
+            (let ((org-num-skip-commented t)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '("2 " "1 ")
+          (org-test-with-temp-text "* H1\n* COMMENT H2"
+            (let ((org-num-skip-commented nil)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Skip commented sub-trees.
+  (should
+   (equal '(nil nil)
+          (org-test-with-temp-text "* COMMENT H1\n** H2"
+            (let ((org-num-skip-commented t)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Skip footnotes sections.
+  (should
+   (equal '(nil "1 ")
+          (org-test-with-temp-text "* H1\n* FN"
+            (let ((org-num-skip-footnotes t)
+                  (org-footnote-section "FN"))
+              (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '("2 " "1 ")
+          (org-test-with-temp-text "* H1\n* FN"
+            (let ((org-num-skip-footnotes nil)
+                  (org-footnote-section "FN"))
+              (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Skip tags, recursively.
+  (should
+   (equal '(nil "1 ")
+          (org-test-with-temp-text "* H1\n* H2 :foo:"
+            (let ((org-num-skip-tags '("foo"))) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '(nil nil)
+          (org-test-with-temp-text "* H1 :foo:\n** H2"
+            (let ((org-num-skip-tags '("foo"))) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Skip unnumbered sections.
+  (should
+   (equal '(nil "1 ")
+          (org-test-with-temp-text
+              "* H1\n* H2\n:PROPERTIES:\n:UNNUMBERED: t\n:END:"
+            (let ((org-num-skip-unnumbered t)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '("2 " "1 ")
+          (org-test-with-temp-text
+              "* H1\n* H2\n:PROPERTIES:\n:UNNUMBERED: t\n:END:"
+            (let ((org-num-skip-unnumbered nil)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '("2 " "1 ")
+          (org-test-with-temp-text
+              "* H1\n* H2\n:PROPERTIES:\n:UNNUMBERED: nil\n:END:"
+            (let ((org-num-skip-unnumbered t)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Skip unnumbered sub-trees.
+  (should
+   (equal '(nil nil)
+          (org-test-with-temp-text
+              "* H1\n:PROPERTIES:\n:UNNUMBERED: t\n:END:\n** H2"
+            (let ((org-num-skip-unnumbered t)) (org-num-mode 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max)))))))
+
+(ert-deftest test-org-num/update ()
+  "Test numbering update after a buffer modification."
+  ;; Headlines created at BEG.
+  (should
+   (equal "1 "
+          (org-test-with-temp-text "X* H"
+            (org-num-mode 1)
+            (delete-char 1)
+            (overlay-get (car (overlays-at (line-beginning-position)))
+                         'after-string))))
+  (should
+   (equal "1 "
+          (org-test-with-temp-text "*<point>\n H"
+            (org-num-mode 1)
+            (delete-char 1)
+            (overlay-get (car (overlays-at (line-beginning-position)))
+                         'after-string))))
+  (should
+   (equal "1 "
+          (org-test-with-temp-text "*<point>bold*"
+            (org-num-mode 1)
+            (insert " ")
+            (overlay-get (car (overlays-at (line-beginning-position)))
+                         'after-string))))
+  ;; Headlines created at END.
+  (should
+   (equal '("1 ")
+          (org-test-with-temp-text "X<point> H"
+            (org-num-mode 1)
+            (insert "\n*")
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '("1 ")
+          (org-test-with-temp-text "X<point>* H"
+            (org-num-mode 1)
+            (insert "\n")
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Headlines created between BEG and END.
+  (should
+   (equal '("1.1 " "1 ")
+          (org-test-with-temp-text ""
+            (org-num-mode 1)
+            (insert "\n* H\n** H2")
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Change level of a headline.
+  (should
+   (equal '("0.1 ")
+          (org-test-with-temp-text "* H"
+            (org-num-mode 1)
+            (insert "*")
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '("1 ")
+          (org-test-with-temp-text "*<point>* H"
+            (org-num-mode 1)
+            (delete-char 1)
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Alter skip state.
+  (should
+   (equal '("1 ")
+          (org-test-with-temp-text "* H :fo<point>o:"
+            (let ((org-num-skip-tags '("foo")))
+              (org-num-mode 1)
+              (delete-char 1))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  (should
+   (equal '(nil)
+          (org-test-with-temp-text "* H :fo<point>:"
+            (let ((org-num-skip-tags '("foo")))
+              (org-num-mode 1)
+              (insert "o"))
+            (mapcar (lambda (o) (overlay-get o 'after-string))
+                    (overlays-in (point-min) (point-max))))))
+  ;; Invalidate an overlay and insert new headlines.
+  (should
+   (equal '("1.2 " "1.1 " "1 ")
+          (org-test-with-temp-text
+              "* H\n:PROPERTIES:\n:UNNUMBE<point>RED: t\n:END:"
+            (let ((org-num-skip-unnumbered t))
+              (org-num-mode 1)
+              (insert "\n** H2\n** H3\n")
+              (mapcar (lambda (o) (overlay-get o 'after-string))
+                      (overlays-in (point-min) (point-max)))))))
+  ;; Invalidate two overlays: current headline and next one.
+  (should
+   (equal '("1 ")
+          (org-test-with-temp-text
+              "* H\n:PROPERTIES:\n:UNNUMBE<point>RED: t\n:END:\n** H2"
+            (let ((org-num-skip-unnumbered t))
+              (org-num-mode 1)
+              (delete-region (point) (line-beginning-position 3))
+              (mapcar (lambda (o) (overlay-get o 'after-string))
+                      (overlays-in (point-min) (point-max))))))))
+
+(provide 'test-org-num)
+;;; org-test-num.el ends here