778 lines
30 KiB
EmacsLisp
778 lines
30 KiB
EmacsLisp
;;; org-protocol.el --- Intercept Calls from Emacsclient to Trigger Custom Actions -*- lexical-binding: t; -*-
|
|
;;
|
|
;; Copyright (C) 2008-2024 Free Software Foundation, Inc.
|
|
;;
|
|
;; Authors: Bastien Guerry <bzg@gnu.org>
|
|
;; Daniel M German <dmg AT uvic DOT org>
|
|
;; Sebastian Rose <sebastian_rose AT gmx DOT de>
|
|
;; Ross Patterson <me AT rpatterson DOT net>
|
|
;; Maintainer: Sebastian Rose <sebastian_rose AT gmx DOT de>
|
|
;; Keywords: org, emacsclient, text
|
|
|
|
;; This file is part of GNU Emacs.
|
|
;;
|
|
;; GNU Emacs is free software: you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
;; GNU Emacs is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; Commentary:
|
|
;;
|
|
;; Intercept calls from emacsclient to trigger custom actions.
|
|
;;
|
|
;; This is done by advising `server-visit-files' to scan the list of filenames
|
|
;; for `org-protocol-the-protocol' and sub-protocols defined in
|
|
;; `org-protocol-protocol-alist' and `org-protocol-protocol-alist-default'.
|
|
;;
|
|
;; Any application that supports calling external programs with an URL
|
|
;; as argument could use this functionality. For example, you can
|
|
;; configure bookmarks in your web browser to send a link to the
|
|
;; current page to Org and create a note from it using `org-capture'.
|
|
;; See Info node `(org) Protocols' for more information.
|
|
;;
|
|
;;
|
|
;; Usage:
|
|
;; ------
|
|
;;
|
|
;; 1.) Add this to your init file (.emacs probably):
|
|
;;
|
|
;; (require 'org-protocol)
|
|
;;
|
|
;; 2.) Ensure emacs-server is up and running.
|
|
;; 3.) Try this from the command line (adjust the URL as needed):
|
|
;;
|
|
;; $ emacsclient \
|
|
;; "org-protocol://store-link?url=http:%2F%2Flocalhost%2Findex.html&title=The%20title"
|
|
;;
|
|
;; 4.) Optionally, add custom sub-protocols and handlers:
|
|
;;
|
|
;; (setq org-protocol-protocol-alist
|
|
;; '(("my-protocol"
|
|
;; :protocol "my-protocol"
|
|
;; :function my-protocol-handler-function)))
|
|
;;
|
|
;; A "sub-protocol" will be found in URLs like this:
|
|
;;
|
|
;; org-protocol://sub-protocol?key=val&key2=val2
|
|
;;
|
|
;; If it works, you can now setup other applications for using this feature.
|
|
;;
|
|
;;
|
|
;; Firefox users follow the steps documented on
|
|
;; https://kb.mozillazine.org/Register_protocol, Opera setup is
|
|
;; described here: http://www.opera.com/support/kb/view/535/
|
|
;;
|
|
;; See also: https://orgmode.org/worg/org-contrib/org-protocol.html
|
|
;;
|
|
;; Documentation
|
|
;; -------------
|
|
;;
|
|
;; org-protocol.el comes with and installs handlers to open sources of published
|
|
;; online content, store and insert the browser's URLs and cite online content
|
|
;; by clicking on a bookmark in Firefox, Opera and probably other browsers and
|
|
;; applications:
|
|
;;
|
|
;; * `org-protocol-open-source' uses the sub-protocol \"open-source\" and maps
|
|
;; URLs to local filenames defined in `org-protocol-project-alist'.
|
|
;;
|
|
;; * `org-protocol-store-link' stores an Org link (if Org is present) and
|
|
;; pushes the browsers URL to the `kill-ring' for yanking. This handler is
|
|
;; triggered through the sub-protocol \"store-link\".
|
|
;;
|
|
;; * Call `org-protocol-capture' by using the sub-protocol \"capture\". If
|
|
;; Org is loaded, Emacs will pop-up a capture buffer and fill the
|
|
;; template with the data provided. I.e. the browser's URL is inserted as an
|
|
;; Org-link of which the page title will be the description part. If text
|
|
;; was select in the browser, that text will be the body of the entry.
|
|
;;
|
|
;; You may use the same bookmark URL for all those standard handlers and just
|
|
;; adjust the sub-protocol used:
|
|
;;
|
|
;; javascript:location.href='org-protocol://sub-protocol?'+
|
|
;; new URLSearchParams({
|
|
;; url: location.href,
|
|
;; title: document.title,
|
|
;; body: window.getSelection()})
|
|
;;
|
|
;; Alternatively use the following expression that encodes space as \"%20\"
|
|
;; instead of \"+\", so it is compatible with Org versions from 9.0 to 9.4:
|
|
;;
|
|
;; location.href='org-protocol://sub-protocol?url='+
|
|
;; encodeURIComponent(location.href)+'&title='+
|
|
;; encodeURIComponent(document.title)+'&body='+
|
|
;; encodeURIComponent(window.getSelection())
|
|
;;
|
|
;; The handler for the sub-protocol \"capture\" detects an optional template
|
|
;; char that, if present, triggers the use of a special template.
|
|
;; Example:
|
|
;;
|
|
;; location.href='org-protocol://capture?'+
|
|
;; new URLSearchParams({template:'x', /* ... */})
|
|
;;
|
|
;; or
|
|
;;
|
|
;; location.href='org-protocol://capture?template=x'+ ...
|
|
;;
|
|
;; uses template ?x.
|
|
;;
|
|
;; Note that using double slashes is optional from org-protocol.el's point of
|
|
;; view because emacsclient squashes the slashes to one.
|
|
;;
|
|
;;; Code:
|
|
|
|
(require 'org-macs)
|
|
(org-assert-version)
|
|
|
|
(require 'org)
|
|
(require 'ol)
|
|
|
|
(declare-function org-publish-get-project-from-filename "ox-publish"
|
|
(filename &optional up))
|
|
|
|
(defvar org-capture-link-is-already-stored)
|
|
(defvar org-capture-templates)
|
|
|
|
(defgroup org-protocol nil
|
|
"Intercept calls from emacsclient to trigger custom actions.
|
|
|
|
This is done by advising `server-visit-files' to scan the list of filenames
|
|
for `org-protocol-the-protocol' and sub-protocols defined in
|
|
`org-protocol-protocol-alist' and `org-protocol-protocol-alist-default'."
|
|
:version "22.1"
|
|
:group 'convenience
|
|
:group 'org)
|
|
|
|
|
|
;;; Variables:
|
|
|
|
(defconst org-protocol-protocol-alist-default
|
|
'(("org-capture" :protocol "capture" :function org-protocol-capture :kill-client t)
|
|
("org-store-link" :protocol "store-link" :function org-protocol-store-link)
|
|
("org-open-source" :protocol "open-source" :function org-protocol-open-source))
|
|
"Default protocols to use.
|
|
See `org-protocol-protocol-alist' for a description of this variable.")
|
|
|
|
(defconst org-protocol-the-protocol "org-protocol"
|
|
"This is the protocol to detect if org-protocol.el is loaded.
|
|
`org-protocol-protocol-alist-default' and `org-protocol-protocol-alist' hold
|
|
the sub-protocols that trigger the required action. You will have to define
|
|
just one protocol handler OS-wide (MS-Windows) or per application (Linux).
|
|
That protocol handler should call emacsclient.")
|
|
|
|
;;; User variables:
|
|
|
|
(defcustom org-protocol-reverse-list-of-files t
|
|
"Non-nil means re-reverse the list of filenames passed on the command line.
|
|
The filenames passed on the command line are passed to the emacs-server in
|
|
reverse order. Set to t (default) to re-reverse the list, i.e. use the
|
|
sequence on the command line. If nil, the sequence of the filenames is
|
|
unchanged."
|
|
:type 'boolean)
|
|
|
|
(defcustom org-protocol-project-alist nil
|
|
"Map URLs to local filenames for `org-protocol-open-source' (open-source).
|
|
|
|
Each element of this list must be of the form:
|
|
|
|
(module-name :property value property: value ...)
|
|
|
|
where module-name is an arbitrary name. All the values are strings.
|
|
|
|
Possible properties are:
|
|
|
|
:online-suffix - the suffix to strip from the published URLs
|
|
:working-suffix - the replacement for online-suffix
|
|
:base-url - the base URL, e.g. https://www.example.com/project/
|
|
Last slash required.
|
|
:working-directory - the local working directory. This is what
|
|
base-url will be replaced with.
|
|
:redirects - A list of cons cells, each of which maps a
|
|
regular expression to match to a path relative
|
|
to `:working-directory'.
|
|
|
|
Example:
|
|
|
|
(setq org-protocol-project-alist
|
|
\\='((\"https://orgmode.org/worg/\"
|
|
:online-suffix \".php\"
|
|
:working-suffix \".org\"
|
|
:base-url \"https://orgmode.org/worg/\"
|
|
:working-directory \"/home/user/org/Worg/\")
|
|
(\"localhost org-notes/\"
|
|
:online-suffix \".html\"
|
|
:working-suffix \".org\"
|
|
:base-url \"http://localhost/org/\"
|
|
:working-directory \"/home/user/org/\"
|
|
:rewrites ((\"org/?$\" . \"index.php\")))
|
|
(\"Hugo based blog\"
|
|
:base-url \"https://www.site.com/\"
|
|
:working-directory \"~/site/content/post/\"
|
|
:online-suffix \".html\"
|
|
:working-suffix \".md\"
|
|
:rewrites ((\"\\(https://site.com/[0-9]+/[0-9]+/[0-9]+/\\)\"
|
|
. \".md\")))
|
|
(\"GNU emacs OpenGrok\"
|
|
:base-url \"https://opengrok.housegordon.com/source/xref/emacs/\"
|
|
:working-directory \"~/dev/gnu-emacs/\")))
|
|
|
|
The :rewrites line of \"localhost org-notes\" entry tells
|
|
`org-protocol-open-source' to open /home/user/org/index.php,
|
|
if the URL cannot be mapped to an existing file, and ends with
|
|
either \"org\" or \"org/\". The \"GNU emacs OpenGrok\" entry
|
|
does not include any suffix properties, allowing local source
|
|
file to be opened as found by OpenGrok.
|
|
|
|
Consider using the interactive functions `org-protocol-create'
|
|
and `org-protocol-create-for-org' to help you filling this
|
|
variable with valid contents."
|
|
:type 'alist)
|
|
|
|
(defcustom org-protocol-protocol-alist nil
|
|
"Register custom handlers for org-protocol.
|
|
|
|
Each element of this list must be of the form:
|
|
|
|
(module-name :protocol protocol :function func :kill-client nil)
|
|
|
|
protocol - protocol to detect in a filename without trailing
|
|
colon and slashes. See rfc1738 section 2.1 for more
|
|
on this. If you define a protocol \"my-protocol\",
|
|
`org-protocol-check-filename-for-protocol' will search
|
|
filenames for \"org-protocol:/my-protocol\" and
|
|
trigger your action for every match. `org-protocol'
|
|
is defined in `org-protocol-the-protocol'. Double and
|
|
triple slashes are compressed to one by emacsclient.
|
|
|
|
function - function that handles requests with protocol and takes
|
|
one argument. If a new-style link (key=val&key2=val2)
|
|
is given, the argument will be a property list with
|
|
the values from the link. If an old-style link is
|
|
given (val1/val2), the argument will be the filename
|
|
with all protocols stripped.
|
|
|
|
If the function returns nil, emacsclient and -server
|
|
do nothing. Any non-nil return value is considered a
|
|
valid filename and thus passed to the server.
|
|
|
|
`org-protocol.el' provides some support for handling
|
|
old-style filenames, if you follow the conventions
|
|
used for the standard handlers in
|
|
`org-protocol-protocol-alist-default'. See
|
|
`org-protocol-parse-parameters'.
|
|
|
|
kill-client - If t, kill the client immediately, once the sub-protocol is
|
|
detected. This is necessary for actions that can be interrupted by
|
|
`C-g' to avoid dangling emacsclients. Note that all other command
|
|
line arguments but the this one will be discarded. Greedy handlers
|
|
still receive the whole list of arguments though.
|
|
|
|
Here is an example:
|
|
|
|
(setq org-protocol-protocol-alist
|
|
\\='((\"my-protocol\"
|
|
:protocol \"my-protocol\"
|
|
:function my-protocol-handler-function)
|
|
(\"your-protocol\"
|
|
:protocol \"your-protocol\"
|
|
:function your-protocol-handler-function)))"
|
|
:type '(alist))
|
|
|
|
(defcustom org-protocol-default-template-key nil
|
|
"The default template key to use.
|
|
This is usually a single character string but can also be a
|
|
string with two characters."
|
|
:type '(choice (const nil) (string)))
|
|
|
|
(defcustom org-protocol-data-separator "/+\\|\\?"
|
|
"The default data separator to use.
|
|
This should be a single regexp string."
|
|
:version "24.4"
|
|
:package-version '(Org . "8.0")
|
|
:type 'regexp)
|
|
|
|
;;; Helper functions:
|
|
|
|
(defun org-protocol-sanitize-uri (uri)
|
|
"Sanitize slashes to double-slashes in URI.
|
|
Emacsclient compresses double and triple slashes."
|
|
(when (string-match "^\\([a-z]+\\):/" uri)
|
|
(let* ((splitparts (split-string uri "/+")))
|
|
(setq uri (concat (car splitparts) "//"
|
|
(mapconcat #'identity (cdr splitparts) "/")))))
|
|
uri)
|
|
|
|
(defun org-protocol-split-data (data &optional unhexify separator)
|
|
"Split the DATA argument for an org-protocol handler function.
|
|
If UNHEXIFY is non-nil, hex-decode each split part. If UNHEXIFY
|
|
is a function, use that function to decode each split part. The
|
|
string is split at each occurrence of SEPARATOR (regexp). If no
|
|
SEPARATOR is specified or SEPARATOR is nil, assume \"/+\". The
|
|
results of that splitting are returned as a list."
|
|
(let* ((sep (or separator "/+\\|\\?"))
|
|
(split-parts (split-string data sep)))
|
|
(cond ((not unhexify) split-parts)
|
|
((fboundp unhexify) (mapcar unhexify split-parts))
|
|
(t (mapcar #'org-link-decode split-parts)))))
|
|
|
|
(defun org-protocol-flatten-greedy (param-list &optional strip-path replacement)
|
|
"Transform PARAM-LIST into a flat list for greedy handlers.
|
|
|
|
Greedy handlers might receive a list like this from emacsclient:
|
|
\((\"/dir/org-protocol:/greedy:/~/path1\" (23 . 12)) (\"/dir/param\"))
|
|
where \"/dir/\" is the absolute path to emacsclient's working directory. This
|
|
function transforms it into a flat list using `flatten-tree' and
|
|
transforms the elements of that list as follows:
|
|
|
|
If STRIP-PATH is non-nil, remove the \"/dir/\" prefix from all members of
|
|
param-list.
|
|
|
|
If REPLACEMENT is string, replace the \"/dir/\" prefix with it.
|
|
|
|
The first parameter, the one that contains the protocols, is always changed.
|
|
Everything up to the end of the protocols is stripped.
|
|
|
|
Note, that this function will always behave as if
|
|
`org-protocol-reverse-list-of-files' was set to t and the returned list will
|
|
reflect that. emacsclient's first parameter will be the first one in the
|
|
returned list."
|
|
(let* ((l (org--flatten-tree (if org-protocol-reverse-list-of-files
|
|
param-list
|
|
(reverse param-list))))
|
|
(trigger (car l))
|
|
(len 0)
|
|
dir
|
|
ret)
|
|
(when (string-match "^\\(.*\\)\\(org-protocol:/+[a-zA-Z0-9][-_a-zA-Z0-9]*:/+\\)\\(.*\\)" trigger)
|
|
(setq dir (match-string 1 trigger))
|
|
(setq len (length dir))
|
|
(setcar l (concat dir (match-string 3 trigger))))
|
|
(if strip-path
|
|
(progn
|
|
(dolist (e l ret)
|
|
(setq ret
|
|
(append ret
|
|
(list
|
|
(if (stringp e)
|
|
(if (stringp replacement)
|
|
(setq e (concat replacement (substring e len)))
|
|
(setq e (substring e len)))
|
|
e)))))
|
|
ret)
|
|
l)))
|
|
|
|
(define-obsolete-function-alias 'org-protocol-flatten
|
|
(if (fboundp 'flatten-tree) 'flatten-tree 'org--flatten-tree)
|
|
"9.7"
|
|
"Transform LIST into a flat list.
|
|
|
|
Greedy handlers might receive a list like this from emacsclient:
|
|
\((\"/dir/org-protocol:/greedy:/~/path1\" (23 . 12)) (\"/dir/param\"))
|
|
where \"/dir/\" is the absolute path to emacsclients working directory.
|
|
This function transforms it into a flat list.")
|
|
|
|
(defun org-protocol-parse-parameters (info &optional new-style default-order)
|
|
"Return a property list of parameters from INFO.
|
|
If NEW-STYLE is non-nil, treat INFO as a query string (ex:
|
|
url=URL&title=TITLE). If old-style links are used (ex:
|
|
org-protocol://store-link/url/title), assign them to attributes
|
|
following DEFAULT-ORDER.
|
|
|
|
If no DEFAULT-ORDER is specified, return the list of values.
|
|
|
|
If INFO is already a property list, return it unchanged."
|
|
(if (listp info)
|
|
info
|
|
(if new-style
|
|
(let ((data (org-protocol-convert-query-to-plist info))
|
|
result)
|
|
(while data
|
|
(setq result
|
|
(append result
|
|
(list (pop data) (org-link-decode (pop data))))))
|
|
result)
|
|
(let ((data (org-protocol-split-data info t org-protocol-data-separator)))
|
|
(if default-order
|
|
(org-protocol-assign-parameters data default-order)
|
|
data)))))
|
|
|
|
(defun org-protocol-assign-parameters (data default-order)
|
|
"Return a property list of parameters from DATA.
|
|
Key names are taken from DEFAULT-ORDER, which should be a list of
|
|
symbols. If DEFAULT-ORDER is shorter than the number of values
|
|
specified, the rest of the values are treated as :key value pairs."
|
|
(let (result)
|
|
(while default-order
|
|
(setq result
|
|
(append result
|
|
(list (pop default-order)
|
|
(pop data)))))
|
|
(while data
|
|
(setq result
|
|
(append result
|
|
(list (intern (concat ":" (pop data)))
|
|
(pop data)))))
|
|
result))
|
|
|
|
;;; Standard protocol handlers:
|
|
|
|
(defun org-protocol-store-link (fname)
|
|
"Process an org-protocol://store-link style url.
|
|
Additionally store a browser URL as an org link. Also pushes the
|
|
link's URL to the `kill-ring'.
|
|
|
|
Parameters: url, title (optional), body (optional)
|
|
|
|
Old-style links such as org-protocol://store-link://URL/TITLE are
|
|
also recognized.
|
|
|
|
The location for a browser's bookmark may look like this:
|
|
|
|
javascript:location.href = \\='org-protocol://store-link?\\=' +
|
|
new URLSearchParams({url:location.href, title:document.title});
|
|
|
|
or to keep compatibility with Org versions from 9.0 to 9.4 it may be:
|
|
|
|
javascript:location.href = \\
|
|
\\='org-protocol://store-link?url=\\=' + \\
|
|
encodeURIComponent(location.href) + \\='&title=\\=' + \\
|
|
encodeURIComponent(document.title);
|
|
|
|
Don't use `escape()'! Use `encodeURIComponent()' instead. The
|
|
title of the page could contain slashes and the location
|
|
definitely will. Org 9.4 and earlier could not decode \"+\"
|
|
to space, that is why less readable latter expression may be necessary
|
|
for backward compatibility.
|
|
|
|
The sub-protocol used to reach this function is set in
|
|
`org-protocol-protocol-alist'.
|
|
|
|
FNAME should be a property list. If not, an old-style link of the
|
|
form URL/TITLE can also be used."
|
|
(let* ((splitparts (org-protocol-parse-parameters fname nil '(:url :title)))
|
|
(uri (org-protocol-sanitize-uri (plist-get splitparts :url)))
|
|
(title (plist-get splitparts :title)))
|
|
(when (boundp 'org-stored-links)
|
|
(push (list uri title) org-stored-links))
|
|
(kill-new uri)
|
|
(message "`%s' to insert new Org link, `%s' to insert %S"
|
|
(substitute-command-keys "\\[org-insert-link]")
|
|
(substitute-command-keys "\\[yank]")
|
|
uri))
|
|
nil)
|
|
|
|
(defun org-protocol-capture (info)
|
|
"Process an org-protocol://capture style url with INFO.
|
|
|
|
The sub-protocol used to reach this function is set in
|
|
`org-protocol-protocol-alist'.
|
|
|
|
This function detects an URL, title and optional text, separated
|
|
by `/'. The location for a browser's bookmark looks like this:
|
|
|
|
javascript:location.href = \\='org-protocol://capture?\\=' +
|
|
new URLSearchParams({
|
|
url: location.href,
|
|
title: document.title,
|
|
body: window.getSelection()})
|
|
|
|
or to keep compatibility with Org versions from 9.0 to 9.4:
|
|
|
|
javascript:location.href = \\='org-protocol://capture?url=\\='+ \\
|
|
encodeURIComponent(location.href) + \\='&title=\\=' + \\
|
|
encodeURIComponent(document.title) + \\='&body=\\=' + \\
|
|
encodeURIComponent(window.getSelection())
|
|
|
|
By default, it uses the character `org-protocol-default-template-key',
|
|
which should be associated with a template in `org-capture-templates'.
|
|
You may specify the template with a template= query parameter, like this:
|
|
|
|
javascript:location.href = \\='org-protocol://capture?template=b\\='+ ...
|
|
|
|
Now template ?b will be used."
|
|
(let* ((parts
|
|
(pcase (org-protocol-parse-parameters info)
|
|
;; New style links are parsed as a plist.
|
|
((let `(,(pred keywordp) . ,_) info) info)
|
|
;; Old style links, with or without template key, are
|
|
;; parsed as a list of strings.
|
|
(p
|
|
(let ((k (if (= 1 (length (car p)))
|
|
'(:template :url :title :body)
|
|
'(:url :title :body))))
|
|
(org-protocol-assign-parameters p k)))))
|
|
(template (or (plist-get parts :template)
|
|
org-protocol-default-template-key))
|
|
(url (and (plist-get parts :url)
|
|
(org-protocol-sanitize-uri (plist-get parts :url))))
|
|
(type (and url
|
|
(string-match "^\\([a-z]+\\):" url)
|
|
(match-string 1 url)))
|
|
(title (or (plist-get parts :title) ""))
|
|
(region (or (plist-get parts :body) ""))
|
|
(orglink
|
|
(if (null url) title
|
|
(org-link-make-string url (or (org-string-nw-p title) url))))
|
|
;; Avoid call to `org-store-link'.
|
|
(org-capture-link-is-already-stored t))
|
|
;; Only store link if there's a URL to insert later on.
|
|
(when url (push (list url title) org-stored-links))
|
|
(org-link-store-props :type type
|
|
:link url
|
|
:description title
|
|
:annotation orglink
|
|
:initial region
|
|
:query parts)
|
|
(raise-frame)
|
|
(org-capture nil template)
|
|
(message "Item captured.")
|
|
;; Make sure we do not return a string, as `server-visit-files',
|
|
;; through `server-edit', would interpret it as a file name.
|
|
nil))
|
|
|
|
(defun org-protocol-convert-query-to-plist (query)
|
|
"Convert QUERY key=value pairs in the URL to a property list."
|
|
(when query
|
|
(let ((plus-decoded (replace-regexp-in-string "\\+" " " query t t)))
|
|
(cl-mapcan (lambda (x)
|
|
(let ((c (split-string x "=")))
|
|
(list (intern (concat ":" (car c))) (cadr c))))
|
|
(split-string plus-decoded "&")))))
|
|
|
|
(defun org-protocol-open-source (fname)
|
|
"Process an org-protocol://open-source?url= style URL with FNAME.
|
|
|
|
Change a filename by mapping URLs to local filenames as set
|
|
in `org-protocol-project-alist'.
|
|
|
|
The location for a browser's bookmark should look like this:
|
|
|
|
javascript:location.href = \\='org-protocol://open-source?\\=' +
|
|
new URLSearchParams({url: location.href})
|
|
|
|
or if you prefer to keep compatibility with older Org versions (9.0 to 9.4),
|
|
consider the following expression:
|
|
|
|
javascript:location.href = \\='org-protocol://open-source?url=\\=' + \\
|
|
encodeURIComponent(location.href)"
|
|
;; As we enter this function for a match on our protocol, the return value
|
|
;; defaults to nil.
|
|
(let (;; (result nil)
|
|
(f (org-protocol-sanitize-uri
|
|
(plist-get (org-protocol-parse-parameters fname nil '(:url))
|
|
:url))))
|
|
(catch 'result
|
|
(dolist (prolist org-protocol-project-alist)
|
|
(let* ((base-url (plist-get (cdr prolist) :base-url))
|
|
(wsearch (regexp-quote base-url)))
|
|
|
|
(when (string-match wsearch f)
|
|
(let* ((wdir (plist-get (cdr prolist) :working-directory))
|
|
(strip-suffix (plist-get (cdr prolist) :online-suffix))
|
|
(add-suffix (plist-get (cdr prolist) :working-suffix))
|
|
;; Strip "[?#].*$" if `f' is a redirect with another
|
|
;; ending than strip-suffix here:
|
|
(f1 (substring f 0 (string-match "\\([\\?#].*\\)?$" f)))
|
|
(start-pos (+ (string-match wsearch f1) (length base-url)))
|
|
(end-pos (if strip-suffix
|
|
(string-match (regexp-quote strip-suffix) f1)
|
|
(length f1)))
|
|
;; We have to compare redirects without suffix below:
|
|
(f2 (concat wdir (substring f1 start-pos end-pos)))
|
|
(the-file (if add-suffix (concat f2 add-suffix) f2)))
|
|
|
|
;; Note: the-file may still contain `%C3' et al here because browsers
|
|
;; tend to encode `ä' in URLs to `%25C3' - `%25' being `%'.
|
|
;; So the results may vary.
|
|
|
|
;; -- start redirects --
|
|
(unless (file-exists-p the-file)
|
|
(message "File %s does not exist.\nTesting for rewritten URLs." the-file)
|
|
(let ((rewrites (plist-get (cdr prolist) :rewrites)))
|
|
(when rewrites
|
|
(message "Rewrites found: %S" rewrites)
|
|
(dolist (rewrite rewrites)
|
|
;; Try to match a rewritten URL and map it to
|
|
;; a real file. Compare redirects without
|
|
;; suffix.
|
|
(when (string-match (car rewrite) f1)
|
|
(let ((replacement
|
|
(concat (directory-file-name
|
|
(replace-match "" nil nil f1 1))
|
|
(cdr rewrite))))
|
|
(throw 'result (concat wdir replacement))))))))
|
|
;; -- end of redirects --
|
|
|
|
(if (file-readable-p the-file)
|
|
(throw 'result the-file))
|
|
(if (file-exists-p the-file)
|
|
(message "%s: permission denied!" the-file)
|
|
(message "%s: no such file or directory." the-file))))))
|
|
nil))) ;; FIXME: Really?
|
|
|
|
|
|
;;; Core functions:
|
|
|
|
(defun org-protocol-check-filename-for-protocol (fname restoffiles _client)
|
|
"Check if `org-protocol-the-protocol' and a valid protocol are used in FNAME.
|
|
Sub-protocols are registered in `org-protocol-protocol-alist' and
|
|
`org-protocol-protocol-alist-default'. This is how the matching is done:
|
|
|
|
(string-match \"protocol:/+sub-protocol\\\\(://\\\\|\\\\?\\\\)\" ...)
|
|
|
|
protocol and sub-protocol are regexp-quoted.
|
|
|
|
Old-style links such as \"protocol://sub-protocol://param1/param2\" are
|
|
also recognized.
|
|
|
|
If a matching protocol is found, the protocol is stripped from
|
|
FNAME and the result is passed to the protocol function as the
|
|
first parameter. The second parameter will be non-nil if FNAME
|
|
uses key=val&key2=val2-type arguments, or nil if FNAME uses
|
|
val/val2-type arguments. If the function returns nil, the
|
|
filename is removed from the list of filenames passed from
|
|
emacsclient to the server. If the function returns a non-nil
|
|
value, that value is passed to the server as filename.
|
|
|
|
If the handler function is greedy, RESTOFFILES will also be passed to it.
|
|
|
|
CLIENT is ignored."
|
|
(let ((sub-protocols (append org-protocol-protocol-alist
|
|
org-protocol-protocol-alist-default)))
|
|
(catch 'fname
|
|
(let ((the-protocol (concat (regexp-quote org-protocol-the-protocol)
|
|
":/+")))
|
|
(when (string-match the-protocol fname)
|
|
(dolist (prolist sub-protocols)
|
|
(let ((proto
|
|
(concat the-protocol
|
|
(regexp-quote (plist-get (cdr prolist) :protocol))
|
|
"\\(:/+\\|/*\\?\\)")))
|
|
(when (string-match proto fname)
|
|
(let* ((func (plist-get (cdr prolist) :function))
|
|
(greedy (plist-get (cdr prolist) :greedy))
|
|
(split (split-string fname proto))
|
|
(result (if greedy restoffiles (cadr split)))
|
|
(new-style (not (= ?: (aref (match-string 1 fname) 0)))))
|
|
(when (plist-get (cdr prolist) :kill-client)
|
|
(message "Greedy org-protocol handler. Killing client.")
|
|
;; If not fboundp, there's no client to kill.
|
|
(if (fboundp 'server-edit) (server-edit)))
|
|
(when (fboundp func)
|
|
(unless greedy
|
|
(throw 'fname
|
|
(if new-style
|
|
(funcall func (org-protocol-parse-parameters
|
|
result new-style))
|
|
(warn "Please update your Org Protocol handler \
|
|
to deal with new-style links.")
|
|
(funcall func result))))
|
|
;; Greedy protocol handlers are responsible for
|
|
;; parsing their own filenames.
|
|
(funcall func result)
|
|
(throw 'fname t))))))))
|
|
fname)))
|
|
|
|
(advice-add 'server-visit-files :around #'org--protocol-detect-protocol-server)
|
|
(defun org--protocol-detect-protocol-server (orig-fun files client &rest args)
|
|
"Advice `server-visit-files' to call `org-protocol-check-filename-for-protocol'.
|
|
This function is indented to be used as :around advice for `server-visit-files'."
|
|
(let ((flist (if org-protocol-reverse-list-of-files
|
|
(reverse files)
|
|
files)))
|
|
(catch 'greedy
|
|
(dolist (var flist)
|
|
;; `\' to `/' on windows. FIXME: could this be done any better?
|
|
(let ((fname (expand-file-name (car var))))
|
|
(setq fname (org-protocol-check-filename-for-protocol
|
|
fname (member var flist) client))
|
|
(if (eq fname t) ;; greedy? We need the t return value.
|
|
(progn
|
|
;; FIXME: Doesn't this just ignore all the files before
|
|
;; this one (the remaining ones have been passed to
|
|
;; `org-protocol-check-filename-for-protocol' but not
|
|
;; the ones before).
|
|
(setq files nil)
|
|
(throw 'greedy t))
|
|
(if (stringp fname) ;; probably filename
|
|
(setcar var fname)
|
|
(setq files (delq var files)))))))
|
|
(apply orig-fun files client args)))
|
|
|
|
;;; Org specific functions:
|
|
|
|
(defun org-protocol-create-for-org ()
|
|
"Create an Org protocol project for the current file's project.
|
|
The visited file needs to be part of a publishing project in
|
|
`org-publish-project-alist' for this to work. The function
|
|
delegates most of the work to `org-protocol-create'."
|
|
(interactive)
|
|
(require 'ox-publish)
|
|
(let ((all (or (org-publish-get-project-from-filename buffer-file-name))))
|
|
(if all (org-protocol-create (cdr all))
|
|
(message "%s"
|
|
(substitute-command-keys
|
|
"Not in an Org project. \
|
|
Did you mean `\\[org-protocol-create]'?")))))
|
|
|
|
(defun org-protocol-create (&optional project-plist)
|
|
"Create a new org-protocol project interactively.
|
|
An org-protocol project is an entry in
|
|
`org-protocol-project-alist' which is used by
|
|
`org-protocol-open-source'. Optionally use PROJECT-PLIST to
|
|
initialize the defaults for this project. If PROJECT-PLIST is
|
|
the cdr of an element in `org-publish-project-alist', reuse
|
|
:base-directory, :html-extension and :base-extension."
|
|
(interactive)
|
|
(let ((working-dir (expand-file-name
|
|
(or (plist-get project-plist :base-directory)
|
|
default-directory)))
|
|
(base-url "https://orgmode.org/worg/")
|
|
(strip-suffix (or (plist-get project-plist :html-extension) ".html"))
|
|
(working-suffix (if (plist-get project-plist :base-extension)
|
|
(concat "." (plist-get project-plist :base-extension))
|
|
".org"))
|
|
(insert-default-directory t)
|
|
(minibuffer-allow-text-properties nil))
|
|
|
|
(setq base-url (read-string "Base URL of published content: " base-url nil base-url t))
|
|
(or (string-suffix-p "/" base-url)
|
|
(setq base-url (concat base-url "/")))
|
|
|
|
(setq working-dir
|
|
(expand-file-name
|
|
(read-directory-name "Local working directory: " working-dir working-dir t)))
|
|
(or (string-suffix-p "/" working-dir)
|
|
(setq working-dir (concat working-dir "/")))
|
|
|
|
(setq strip-suffix
|
|
(read-string
|
|
(concat "Extension to strip from published URLs (" strip-suffix "): ")
|
|
strip-suffix nil strip-suffix t))
|
|
|
|
(setq working-suffix
|
|
(read-string
|
|
(concat "Extension of editable files (" working-suffix "): ")
|
|
working-suffix nil working-suffix t))
|
|
|
|
(when (yes-or-no-p "Save the new org-protocol-project to your init file? ")
|
|
(setq org-protocol-project-alist
|
|
(cons `(,base-url . (:base-url ,base-url
|
|
:working-directory ,working-dir
|
|
:online-suffix ,strip-suffix
|
|
:working-suffix ,working-suffix))
|
|
org-protocol-project-alist))
|
|
(customize-save-variable 'org-protocol-project-alist org-protocol-project-alist))))
|
|
|
|
(provide 'org-protocol)
|
|
|
|
;;; org-protocol.el ends here
|