1

I use org-babel-tangle to generate lilypond and latex documents from org files.

These org files have the following first line:

#+PROPERTY: header-args :LANG :comments org :tangle PATH/FILE_NAME.EXT

Where

  • LANG is either latex or lilypond
  • PATH is a specific (each time different) path
  • FILE_NAME is a specific (each time different) file name
  • EXT is either tex or ly

At first, I was compiling the generated .ly/.tex to pdf from the terminal. But then I started putting a function on each org file to generate and open PDFs. Here is the function for tex documents:

  (defun mda/tex-pdf ()
    "Produce a Xelatex PDF from an org file containing tex blocks."
    (interactive)
    (org-babel-tangle)
    (cd "PATH")
    (shell-command "xelatex -interaction=nonstopmode NAME_FILE.tex")
    (shell-command "evince NAME_FILE.pdf"))

I can add easily this function with skeleton, but I have to change PATH once and NAME_FILE twice each time.

I there a way to automatically pick up PATH, and NAME_FILE from the first line? This way I could move the 2 functions to my config file.

EDIT:

It appears that the use case description wasn't precise enough.

So in this use case, every source block in the Org mode file uses the same language and the same :tangle path spec (which is specified once, in #+PROPERTY: header-args :...). So that all the tangled blocks end up in the exact same file.

2
  • 2
    You'll have to do your own loop over code blocks but you can use org-babel-get-src-block-info to get at the header args.
    – NickD
    Commented Sep 15, 2023 at 2:02
  • Thank you @NickD org-babel-get-src-block-info is very useful indeed. However, I can't find how to make it work when called from a function evaluated outside of the tangled buffer (see my edit for more details).
    – crocefisso
    Commented Sep 16, 2023 at 12:34

2 Answers 2

1

If each block in the file uses its own language and/or its own tangle path, then you will have to examine each one separately. That's what I assumed at first and that assumption is behind my original comment: you would have to loop over all of the blocks and treat each one separately.

But after you added clarifications in the comments (which should really be in the question itself), the following answer assumes that every source block in the Org mode file uses the same language and the same :tangle path spec, so that all the tangled blocks end up in the exact same file. There is then no need to examine each source block for those values: they will all have the same values, so getting the values from one of them should be enough.

Proceeding on that assumption, we end up with something like this:

(defun my/get-lang-and-path ()
  "Get lang and path info from the first source block in the file"
  (save-excursion
    (goto-char (point-min))
    (org-babel-next-src-block)
    (let* ((info (org-babel-get-src-block-info))
           (lang (car info))
           (path (cdr (assq :tangle (caddr info)))))
      (if (null info)
          nil
        (list lang path)))))

(defun mda/tangle-tex-compile-open-pdf()
  "Generate and open a PDF from an org file containing tex blocks."
  (interactive)
  (let* ((info (my/get-lang-and-path))
         (lang (car info))
         (path (cadr info))
         (directory (file-name-directory path))
         (filename (file-name-nondirectory path))
         (basename (file-name-base path)))
    ;; tangle all the code blocks
    (org-babel-tangle)
    (let ((default-directory (expand-file-name (or directory "."))))
      ;; generate PDF file - appropriate command depends on language
      (cond
       ((string= lang "latex")
        (shell-command (concat "xelatex -interaction=nonstopmode " filename)))
       ((string= lang "lilypond")
        (shell-command (concat "lilypond " filename)))
       (t
        (message "Unknown language")))
      ;; ...and show it if it exists
      (if (file-exists-p (concat basename ".pdf"))
          (shell-command (concat "evince " basename ".pdf"))
        (message "%s.pdf does not exist in directory %s" basename default-directory)))))

There is no error checking, but the code does not barf with or without code blocks. I tested with

emacs -q -l foo.el foo.org

where foo.el includes the two functions shown above and foo.org looks like this:

#+PROPERTY: header-args :comments org :tangle tangle/foo.tex

* Test

#+name: doc-preamble
#+begin_src latex
  \documentclass{article}

  \begin{document}
#+end_src

#+name:doc-body
#+begin_src latex

  \int_0^\infty 1 dx = \infty
#+end_src

#+begin_src latex
\end{document}
#+end_src

or like this:

#+PROPERTY: header-args :comments org :tangle tangle/foo.tex

* Test

No code blocks here.

I get reasonable behavior in either case, although the error handling in the second case is insufficient for "production" use.

4
  • Thank you so much for your help NickD! I edited the question as you recommended. Your code works perfectly. I will now try to extend the use case, so your code can work even when a path isn't provided (as for now, with #+PROPERTY: header-args :comments org :tangle FILE-NAME.EXT the function shows the following error let: Wrong type argument: stringp, nil).
    – crocefisso
    Commented Sep 18, 2023 at 12:14
  • 1
    I made a change to the expand-file-name call to catch this case. Try it out and let me know.
    – NickD
    Commented Sep 18, 2023 at 16:00
  • 1
    Brilliant, thank you! I was trying to implement it with an if, but your solution is much smarter. I changed (shell-command (concat "evince " basename ".pdf")) to (shell-command (concat "evince " basename ".pdf &")), in order to run evince in background, so the org file can still be edited.
    – crocefisso
    Commented Sep 18, 2023 at 16:54
  • 1
    Yes, thre is plenty of space for improvement and elaboration. I'm sure there will be other cases where it fails too (but if you run into them, I'll do my best to fix them).
    – NickD
    Commented Sep 18, 2023 at 21:18
1

Thank you @NickD, I managed to build a function from the full-path (cdr (assq :tangle (caddr (org-babel-get-src-block-info)))) command that you provided here.

Here it is:

  (defun mda/tangle-tex-compile-open-pdf()
    "Generate and open a PDF from an org file containing tex blocks."
    (interactive)
    (org-babel-tangle)
    (let ((mda/var (cdr (assq :tangle (caddr(org-babel-get-src-block-info))))))
      (cd (file-name-directory mda/var))
      (shell-command (concat "xelatex -interaction=nonstopmode " (file-name-nondirectory mda/var)))
      (shell-command (concat "evince " (file-name-base mda/var) ".pdf"))))

Oddly, this function only works when it is evaluated from the buffer where the #+PROPERTY: header-args :LANG :comments org :tangle PATH/FILE_NAME.EXT is located. When it is evaluated from init, running it gives the following error: cd: Wrong type argument: stringp, nil. Meaning that mda/var is nil...

9
  • There is nothing odd about that if you call the function from your init file: it will only work if the current buffer is an Org mode file where each code block has a :tangle header arg of the appropriate form (either individually or set with a property). You can define the function in your init file, but you can not call it there. In addition, as I mentioned, you'll have to implement your own loop: org-babel-get-src-block assumes that it is called with point in the source block: if you call it from elsewhere, it will return nil, so you cannot call org-babel-tangle...
    – NickD
    Commented Sep 16, 2023 at 12:46
  • You'll have to use a mapping function (like org-babel-map-src-blocks) and evaluate a function that tangles the current source block only (using org-babel-get-src-block-info on the current block to glean the info you need).
    – NickD
    Commented Sep 16, 2023 at 12:50
  • Sorry if I wasn't clear in my explanation. I always call the function from an org file containing latex or lilypond source blocks and #+PROPERTY: header-args :LANG :comments org :tangle PATH/FILE_NAME.EXT in the first line. When the function is compiled inside the tangled buffer it works, but when it is not (when it is only present in the init) it doesn't.
    – crocefisso
    Commented Sep 16, 2023 at 12:52
  • So it's the second comment that is applicable: you'll have to implement your own loop using the mapping function - you cannot call org-babel-tangle to tangle them all first.
    – NickD
    Commented Sep 16, 2023 at 12:58
  • You cannot use a single #+PROPERTY line for every block: LANG and PATH and FILENAME are different for each block - no?
    – NickD
    Commented Sep 16, 2023 at 13:03

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.