I have a very complex (at least to my mind) text replacement task. ideally this would be a function or macro I could run over every org header in a buffer.

I want to 1. move over any TODO state to a line beneath the header starting with tags: 2. then move over any org tags following that todo state into the tags line proceeded by a comma

the issue is that of course the org header changes in depth across the file and the header name changes of course.

this is for getting ready to use org export to markdown. since org markdown export cant convert org tags and states into markdown tags I want this step of pre proceesing. I would then export each changed heading to md and use the first line as meatadata.

the markdown app can the read these exported tags and thus keeping/"exporting" my org rating and todo state .

as an example this org section:

*** ✔ Kopitiam                                                        :3star:
pretty good malasyian food. Had a nice frothy tea, the nasi lamak and blue rice with pandan cream
all was ok but nothing outstanding 

*** Brussels
**** ✔ Tonton Garby                                                   :3star:
very good sandwhices in brussels

would look like this after the find and replace

*** Kopitiam                                                       
tags: #✔ , #3star
pretty good malasyian food. Had a nice frothy tea, the nasi lamak and blue rice with pandan cream
all was ok but nothing outstanding 

*** Brussels
**** Tonton Garby  
tags: #✔ , #3star                                                 
very good sandwhices in brussels

is something like that even possible within emacs or do I need to some other tools like python?


someone helped me try a outside emacs solution using perl

#!/usr/bin/env perl

use strict;
use warnings;

sub fmt_tags {
    return unless defined $+{tags};
    map {q/#/ . $_} $+{tags} =~ /\w+/g

sub check_tag {
    defined $+{checked}? q{#} . $+{checked} : q{}

while (<>) {
        /$+{stars} $+{title}\ntags: @{[join ', ', check_tag(), fmt_tags()]}\n/x;

this seems to work, yet docent really pick up the TOVISIT tags...

  • It's possible, but the resulting file would break the Org mode syntax: the TODO state of the headline and the tags would not be recognized any longer.
    – NickD
    Commented Jan 17, 2020 at 18:32
  • thx so much @nickD. im aware of that. I need to be able to export these to markdown for a project.
    – zeltak
    Commented Jan 17, 2020 at 19:00
  • I don't understand what you mean: what stops you from exporting the original file to Markdown? Also, have you checked pandoc? It can export Org mode files to Markdown of various flavors. But you might want to add more details in your question.
    – NickD
    Commented Jan 17, 2020 at 19:07
  • thx again ill add a clarification in the post. The specific app I use for markdown reads tags in first line as tags: #tag1,#tag2. I want to not loose the org data such as tags and todo state and thus transfer it as "metadata" in my exported md files after I fix the org file.
    – zeltak
    Commented Jan 17, 2020 at 19:17

1 Answer 1


This function maps through all headings with todo state ✔ and inserts the todo state and tags as described (as long as I have understood it correctly):

(defun my-org-prepare-for-markdown-export (&optional _backend)
  "Insert a line below all headings with todo state ✔.
The line has the following format: tags: #✔ , #tag1 , #tag2 , ...
Existing org tags are removed."
  ;; Map through each heading that has a todo state ✔.
   ;; Function called at each heading.
   (lambda ()
     (let* ((todo (org-get-todo-state))
            (tags (org-get-tags nil t))
            ;; Get list of todo and tags if non-nil.
            (lst (if todo
                     (if tags
                         (append (list todo) tags)
                       (list todo))
            ;; Build the string to insert.
            (str (mapconcat (lambda (x) (concat "#" x)) lst " , "))
            (str (concat "tags: " (substring-no-properties str))))
       ;; Remove tags.
       (org-set-tags nil)
       ;; Insert newline and insert text.
       (insert str)))
   ;; Match for headings to map through.

You should add this function to the org-export-before-processing-hook (that's why I added the backend argument). The following command adds it to the hook, exports the buffer to markdown and removes it from the hook afterwards. Like this the original buffer is not touched:

(defun my-org-special-markdown-export ()
  (add-hook 'org-export-before-processing-hook #'my-org-prepare-for-markdown-export)
  ;; Or
  ;; (org-md-export-to-markdown)
  (remove-hook 'org-export-before-processing-hook #'my-org-prepare-for-markdown-export))

Call it with M-x my-org-special-markdown-export while the buffer is active.

  • perfect! thx a lot.
    – zeltak
    Commented Jan 22, 2020 at 0:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.