3

I am using the Emacs editor together with the org-mode and evil-mode mainly for text handling and documentation. Often there is a topic where several different URLs to websites belong to.

Example: I have a text snippet on how to install Emacs:

*** install emacs

emacs - I want to try org-mode. What's the shortest path from zero to typing? - Stack Overflow
https://stackoverflow.com/questions/4940680/i-want-to-try-org-mode-whats-the-shortest-path-from-zero-to-typing

Index of /gnu/emacs/windows/emacs-26
http://ftp.gnu.org/gnu/emacs/windows/emacs-26/emacs-26.3-x86_64.zip

Installation target:
file://C:\Lupo_Pensuite\MyApps\emacs

How to
file://C:\Lupo_Pensuite\MyDocs\howto.txt

Is it possible to select the region and all the URLs are opened within my default web browser? And the file link is being opened by Windows Explorer? And the text file is opened with the associated editor?

Or even better: emacs is aware that the a.m. text snippet actually is a org-mode chapter. And regardless where within that chapter the cursor is positioned, something like M-x open-all-links-in-chapter is...opening all mentioned links in the current chapter.

Prio 1: is there something like that existing in emacs/org-mode/evil-mode already?

Prio 2: is there a elisp function you know which can achieve this use case?

Enviroment: Cygwin under Windows 10, emacs 26.3, org-mode 9.1.9

Alex
  • 61
  • 6
  • That sounds like a fairly bad idea to me: what happens if among those URLs, there are a few videos (say 1GB each) - do you have enough memory to handle them? Will you browser and/or your network survive the onslaught? Will you machine start swapping and become so slow that the only thing to do is cycle the power? Some food for thought... If you still want to do it, I don't think there is anything built-in; somebody might have written a function to do that somewhere already (I wouldn't know); it would be fairly easy to write a function to do that. But do you really want it? – NickD May 05 '20 at 19:59
  • 1
    Hi @NickD, first of all: thanks a lot for looking into it! Now to your question. Definitely I want to have this. In 99% of all cases these links are **no** videos, but links to web pages. And local links. Just documentation. – Alex May 06 '20 at 15:15
  • Yeah, what I'm worried about is that last 1% :-) I'll post a partial answer (getting the list of links but I'll let you worry about the actual sending to the browser which should be straightforward) later on today - no time ATM. – NickD May 06 '20 at 15:41
  • It's actually a full answer (but for http[s] type links only). – NickD May 06 '20 at 18:52

2 Answers2

3

It turns out, that org-mode has this already built-in!

Today I was browsing the documentation of org-mode, wondering how exactly C-c C-o is working. That key combo is calling the emacs org-mode function "org-open-at-point". org-open-at-point is opening the URL where the cursor (in emacs speak: point) is positioned.

Now if a C-c C-o is pressed on a heading, then all URL's beneath that heading are opened! Which is exactly what I asked for from the beginning. Thanks a lot, NickD, for your constructive contributions!

Here the original help text:

When point is on a headline, display a list of every link in the entry, so it is possible to pick one, or all, of them.

Alex
  • 61
  • 6
1

Warning: used without thought, the following can bring your machine to its knees. I will add some more specific warnings at the end, but be careful!

The basic idea of the code below is to parse the buffer of an Org mode file, in order to get a parse tree of the buffer: that is done by org-element-parse-buffer. We can then use org-element-map to walk the parse tree and select only nodes of type link, applying a function to each one as we go. The function we apply, get-link, munges through the contents of the link node, extracting the type and path and returning a list of those two. Here's how it looks so far:

(defun get-link (x)
  (let* ((link (cadr x))
         (type (plist-get link :type))
         (path (plist-get link :path)))
   (if (or (string= type "http") (string= type "https"))
     (list type path))))

(defun visit-all-http-links ()
  (interactive)
  (let* ((parse-tree (org-element-parse-buffer))
         (links (org-element-map parse-tree 'link #'get-link)))
    links))

Note that I only keep http and https links: you may want to add extra types.

This already goes a long way towards getting you what you want. In fact, if you load the file with the two functions above, you can try it on the following sample Org mode file:

* foo
** foo 1
http://www.google.com
https://redhat.com
* bar
** bar 2
[[https://gnome.org][Gnome]] is a FLOSS project. So is Fedora: https://fedoraproject.org.


* Code
#+begin_src emacs-lisp :results value verbatim :wrap example
(visit-all-http-links)
#+end_src

#+RESULTS:
#+begin_example
(("http" "//www.google.com") ("https" "//redhat.com") ("https" "//gnome.org") ("https" "//fedoraproject.com"))
#+end_example

and evaluating the source block with C-c C-c, you get the results shown.

Now all we need to do is convert each (TYPE PATH) pair in the result list to a real URL and then visit it - here's the final version of the code:


(defun get-link (x)
  "Assuming x is a LINK node in an Org mode parse tree,
   return a list consisting of its type (e.g. \"http\")
   and its path."
  (let* ((link (cadr x))
         (type (plist-get link :type))
         (path (plist-get link :path)))
   (if (or (string= type "http") (string= type "https"))
     (list type path))))

(defun format-url (x)
  "Take a (TYPE PATH) list and return a proper URL. Note
   the following works for http- and https-type links, but
   might need modification for other types."
  (format "%s:%s" (nth 0 x) (nth 1 x)))

(defun visit-all-http-links ()
  (interactive)
  (let* ((parse-tree (org-element-parse-buffer))
         (links (org-element-map parse-tree 'link #'get-link)))
    (mapcar #'browse-url (mapcar #'format-url links))))

We add a function format-url that does this: ("http" "//example.com") --> "http://example.com" and map it on the links list, producing a new list of URLS. Then we map the function browse-url (which is provided by emacs) on the resulting list and we watch the browser open them all.

WARNINGS:

  • If you have hundreds or thousands of links in the file, then you are going to create hundreds or thousands of tabs in your browser. Are you SURE your machine can take it?

  • If your links point to big objects, that's going to put another kind of memory pressure on your system. Are you SURE your machine can take it?

  • If your Org mode buffer is big, then org-element-parse-buffer can take a LONG time to process it. Moreover, even though there is a caching mechanism, it is not enabled by default because of bugs, so every time you execute the function you are going to parse the buffer AGAIN from scratch.

  • Every time you execute the function, you are going to open NEW tabs in your browser.

EDIT in response to questions in comments:

Q1: "visit-all-http-links opens all URLs in the file. My original question was, whether it is possible to open only the URLs which are being found in the current org-mode chapter."

A1: Doing just a region is a bit harder but possible, if you guarantee that the region is syntactically correct Org mode (e.g. a collection of headlines and their contents). You just write the region to a temporary buffer and then do what I did on the temp buffer instead of the original. Here's the modified code using the visit-url function from Question 2:

(defun visit-all-http-links-in-region (beg end)
  (interactive "r")
  (let ((s (buffer-substring beg end)))
    (with-temp-buffer
      (set-buffer (current-buffer))
      (insert s)
      (let* ((parse-tree (org-element-parse-buffer))
             (links (org-element-map parse-tree 'link #'get-link)))
        (mapcar #'visit-url (mapcar #'format-url links))))))

(defun visit-all-http-links ()
  (interactive)
  (visit-all-http-links-in-region (point-min) (point-max)))

Very lightly tested.

Q2: "Every time I execute your function with your example URLs, the URLs are being opened with a different sequence - is it possible to open the URLs in that very sequence which is found in the org file?"

A2: The links are found deterministically in the order that they occur in the file. But the moment you call browse-url, all bets are off, because the URL now belongs to the browser, which will try to open each URL it receives in a separate tab and using a separate thread - in other words asynchronously. You might try introducing a delay between calls, but there are no guarantees:

(defun visit-url(url)
   (browse-url)
   (sit-for 1 t))

and then use visit-url instead of browse-url in visit-all-urls.

NickD
  • 5,937
  • 1
  • 21
  • 38
  • Wow! It is working! I enhanced your code a bit by adding the type file:// - now even that is working. I have read your warnings. Thanks for that! Questions: (1) visit-all-http-links opens *all* URLs in the file. My original question was, whether it is possible to open *only* the URLs which are being found in the *current* org-mode *chapter*. When just simply opening *all* URLs in the file, it would not be neccessary to parse the org-mode tree. – Alex May 06 '20 at 20:35
  • 2nd question: Every time I execute your function with your example URLs, the URLs are being opened with a different sequence. Sometimes google is first, sometimes fedora, etc. - is it possible to open the URLs in that very sequence which is found in the org file? In your example that would be: first google, than redhat, than gnome and last fedora. – Alex May 06 '20 at 20:36
  • The *easiest* way to get all the links is by parsing the tree (assuming that somebody *else* has written the parser: fortunately, that's the case here - `org-element` is the official Org mode parser). Otherwise, you have to do your own (usually half-assed) parsing (and that usually ends up with trying to write a regexp to match the things of interest and then complicating it ad infinitum to try to get rid of the false positives/negatives): try to do that in two lines of code as I've done above. Of course, `org-element` is 6K lines of code, but it's already written and debugged. – NickD May 06 '20 at 21:52
  • 1
    Re *"hundreds or thousands of links in the file"*: It is prudent to [limit the number of URLs opened to, say, 50](https://pmortensen.eu/world2/2020/03/29/using-geany#open-urls), in the script to avoid accidentally opening a huge number of URLs (it usually doesn't make sense to open more than 50 at a time anyway). – Peter Mortensen Mar 19 '23 at 19:15