Emacs Function to Assign Org-Mode Property to Matching Criteria

Summary: Some Emacs Lisp to help me populate random information into my Campaign Status document. This delves into the org-element-map function and some querying of information to pick the right elements.

, I wrote my Update on the Campaign Status Document. I had manually set the alignment of several Non-Player Characters (NPCs 📖) ; but I thought “Maybe I should instead clear those out and randomize?”

And I started thinking about how I might automatically update (or add) new properties.

What I wanted was to select a top-level headline, select a property name, and pick a random table. Sidenote As per my random-tables Emacs package 📖 . With those chosen, I then populate all immediate sub-headings with the property and a randomized roll on the table. Sidenote A “table” can be a dice expression.

Below is that implementation.

(defun jf/campaign/populate-property-draw-value (&optional headline property table)
  "Populate HEADLINE's direct children's empty PROPERTY with roll on given TABLE.

When given no HEADLINE, prompt for ones from the document.

When given no PROPERTY, prompt for a property.

When given no TABLE, prompt for an expression to evaluate via
`random-table/roll'."
  (interactive)
  (let* ((level 1)
          (headline (or headline
                      (completing-read "Headline: "
                        (jf/org-get-headlines level) nil t)))
          (property (or property
                      (org-read-property-name)))
          (table (or table
                   (completing-read "Table: "
                     random-table/storage/tables)))
          (random-table/reporter
            ;; Bind a function that will only output the results of the
            ;; table, excluding the expression that generated the
            ;; results.
            (lambda (expression results) (format "%s" results))))
    (unless (org--valid-property-p property)
      (user-error "Invalid property name: \"%s\"" property))
    (save-excursion
      ;; Now that we have the information, let’s start rolling on some
      ;; tables.
      (dolist
        ;; Because we will not be recalculating heading positions, we
        ;; need to reverse the list, as updates to the last element will
        ;; not alter the position of the next to last element.
        (subheading (reverse
                      (org-element-map
                        (org-element-parse-buffer)
                        'headline
                        (lambda (el)
                          (and
                            (= ;; We want the children of the given
			       ;; level
                             (+ 1 level)
			     (org-element-property :level el))
                            (string=
                              ;; Match the text
                              (org-element-property
                                :raw-value
                                (car (org-element-lineage el)))
                              headline)
                            ;; Don't clobber already set values
                            (not (org-element-property property el))
                            ;; We found a match now return it
                            el)))))
        ;; We have finally found the element, now roll on the table and
        ;; populate.
        (org-entry-put
          (goto-char (org-element-property :begin subheading))
          property (random-table/roll table))))))

Conclusion

On the surface, Org-Mode 📖 syntax appears to be a variant of Markdown. But the true beauty shines when you consider the additional structure and functionality. The Org Element API provides useful guidance. But even more valuable is being able to see the source code.

I was able to look at the org-set-property function to find a few bits of useful logic that would help me avoid repetition. And from there I have a reasonably robust function.

One refinement obvious refinement is that the table parameter could instead be a function, which is then called for each element.

update

The previous code worked for a small test case, but once I test against a more complicated situation, it failed.

I intuited there would be a problem, as the previous comments indicated; but in my haste I didn’t check and verify.

I have updated the code and references to reflect those changes.