From mboxrd@z Thu Jan 1 00:00:00 1970 From: Eric Abrahamsen Subject: Re: macro for iterating over headings and "doing things" with them Date: Mon, 29 Sep 2014 18:10:33 +0800 Message-ID: <87lhp27n52.fsf@ericabrahamsen.net> References: <871tqu95m1.fsf@ericabrahamsen.net> Mime-Version: 1.0 Content-Type: text/plain Return-path: Received: from eggs.gnu.org ([2001:4830:134:3::10]:53422) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1XYXqN-0007P3-KP for emacs-orgmode@gnu.org; Mon, 29 Sep 2014 06:06:17 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1XYXqH-0007xt-WB for emacs-orgmode@gnu.org; Mon, 29 Sep 2014 06:06:11 -0400 Received: from plane.gmane.org ([80.91.229.3]:37076) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1XYXqH-0007wW-Ms for emacs-orgmode@gnu.org; Mon, 29 Sep 2014 06:06:05 -0400 Received: from list by plane.gmane.org with local (Exim 4.69) (envelope-from ) id 1XYXqB-0007k4-Tq for emacs-orgmode@gnu.org; Mon, 29 Sep 2014 12:05:59 +0200 Received: from 222.128.163.23 ([222.128.163.23]) by main.gmane.org with esmtp (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for ; Mon, 29 Sep 2014 12:05:59 +0200 Received: from eric by 222.128.163.23 with local (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for ; Mon, 29 Sep 2014 12:05:59 +0200 List-Id: "General discussions about Org-mode." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-orgmode-bounces+geo-emacs-orgmode=m.gmane.org@gnu.org Sender: emacs-orgmode-bounces+geo-emacs-orgmode=m.gmane.org@gnu.org To: emacs-orgmode@gnu.org Eric Abrahamsen writes: > Hi all, > > Recently, with the help of emacs.help, I wrote a small macro called > `org-iter-headings' (essentially a thin wrapper around > `org-element-map') for iterating over the child headings in a subtree, > and "doing something" with them. It's meant to be a quick-and-dirty, > *scratch*-buffer convenience function, for when you have some regular > data in a series of sibling headings, and want to use that data in a > one-off way. So anything from mail merges, to quick collections of > property values, to launching more complex per-heading processes. > > You call the macro on a subtree's parent, and the body of the macro is > executed once for each child heading. In the macro body, the dynamic > variables `head', `item', `todo', `tags', `log-items' and `body-pars' > are bound appropriately (see the docstring below). > > For example, I occasionally evaluate a batch of manuscripts for a > publishing house. Each child heading is a manuscript: the heading text > is the title, the todo either "ACCEPT" or "REJECT", with my reasons in > the todo state log. I might this information to the publishing house by > calling the following on the parent heading: I realized I might have made this example too complicated. You could use this for something as simple as collecting the heading text of all headings in a subtree: (setq these-headings (org-iter-headings item)) Or, slightly more involved, adding up the :AMOUNT: property (a number) of all the headings whose TODOs are not yet done: (apply #'+ (org-iter-headings (when (equal (car todo) 'todo) (string-to-number (org-element-property :AMOUNT head))))) Eric > (let ((buf (get-buffer-create "*temp output*"))) > (with-current-buffer buf > (erase-buffer)) > (org-iter-headings > (with-current-buffer buf > (insert > (format > "Title: %s\nMy recommendation: %s, reasons as follows:\n\n%s\n\n\n" > > item (capitalize (cdr todo)) > ;; Get the most recent state log, and insert all its paragraphs > ;; but the first one. > (mapconcat #'identity (cdar log-items) "\n"))))) > (compose-mail "people@pubhouse.com" > "Manuscript evaluation" ) > (message-goto-body) > (insert-buffer-substring buf)) > > > For things you do regularly, you might as well write a proper function > using `org-element-map'. But for one-offs, this can be a lot easier to > manage. > > I guess I might stick this on Worg, depending on how useful people think > it might be. > > Enjoy, > Eric > > > (defmacro org-iter-headings (&rest body) > "Run BODY forms on each child heading of the heading at point. > > This is a thin wrapper around `org-element-map'. Note that the former > will map over the heading under point as well as its children; this > function skips the heading under point, and *only* applies to the > children. At present it ignores further nested headline. If you have a > strong opinion on how to customize handling of nested children, please > contact the author. > > Within the body of this macro, dynamic variables are bound to > various parts of the heading being processed: > > head: The full parsed heading, as an Org element. Get your > property values here. > > item: The text (ie :raw-value) of the heading. > > todo: The heading's todo information, as a cons of the todo > type (one of the symbols 'todo or 'done) and the todo keyword as > a string. > > tags: The heading's tags, as a list of strings. > > log-items: If org-log-into-drawer is true, and the drawer is > present, then this variable will hold all the list items from the > log drawer. Specifically, each member of `log-items' is a further > list of strings, containing the text of that item's paragraphs. > Not the paragraphs as parsed org structures, just their text. If > org-log-into-drawer is false, any state logs or notes will be > found in body-pars. > > body-pars: A list of all the paragraphs in the heading's body text; > \"paragraphs\" are understood however `org-element-map' > understands them. > > tree: This holds the entire parsed tree for the subtree being > operated on. > > > This macro returns a list of whatever value the final form of > BODY returns." > `(call-org-iter-headings > (lambda (tree head item todo tags log-items body-pars) ,@body))) > > (defun call-org-iter-headings (thunk) > (save-restriction > (org-narrow-to-subtree) > (let ((tree (org-element-parse-buffer)) > (log-spec org-log-into-drawer) > (first t) > returns) > (setq > returns > (org-element-map tree 'headline > (lambda (head) > ;; Skip the first headline, only operate on children. Is > ;; there a less stupid way of doing this? > (if first > (setq first nil) > (let ((item (org-element-property :raw-value head)) > (todo (cons > (org-element-property :todo-type head) > (org-element-property :todo-keyword head))) > (tags (org-element-property :tags head)) > (log-items > (org-element-map > (org-element-map head 'drawer > ;; Find the first drawer called > ;; \"LOGBOOK\", or whatever. > (lambda (d) > (when (string= > (if (stringp log-spec) > log-spec "LOGBOOK") > (org-element-property :drawer-name d)) > d)) > nil t) > ;; Map over the items in the logbook > ;; list. > 'item > (lambda (i) > ;; Map over the paragraphs in each > ;; item, and collect the text. > (org-element-map i 'paragraph > (lambda (p) > (substring-no-properties > (org-element-interpret-data > (org-element-contents p)))))))) > (body-pars (org-element-map head 'paragraph > (lambda (p) > (substring-no-properties > (org-element-interpret-data > (org-element-contents p)))) > nil nil '(headline drawer)))) > ;; Break the log item headings into their own > ;; paragraph. > (setq log-items > (mapcar > (lambda (ls) > (if (string-match-p "\\\\\\\\" (car ls)) > (append (split-string (car ls) "\\\\\\\\\n") > (cdr ls)))) > log-items)) > (funcall thunk tree head item todo tags log-items body-pars)))))) > (delq nil returns))))