Emacs: tone down Org citations on demand

Raw link: https://www.youtube.com/watch?v=X3fEO1_QDHA

This is a short video demonstration of how I tone down Org citations using some custom code I wrote. I share the code and its annotated version. See the following two sections.

The code without any commentary

UPDATE 2024-12-26 20:02 +0200: I updated the code to include the save-excursion, which I forgot to cover in my original publication. The annotated version has the same update.

The annotated version is in the next section.

(defvar prot-org-cite-regexp
  "\\(?1:cite: ?\\)?\\(?2:@\\)\\(?:[a-z]+\\)\\(?3:.*?\\)\\(?:[0-9]\\{4,\\}\\)\\(?:[a-z]\\)?"
  "Regular expression matching an Org citation.
Groups 1, 2, and 3 are meant to be hidden when the minor mode
`prot-org-cite-mode' is enabled.")

(defun prot-org-cite-add-overlays ()
  "Add invisible overlays to `prot-org-cite-regexp' numbered groups."
  (let ((case-fold-search nil))
    (save-excursion
      (save-restriction
        (widen)
        (goto-char (point-min))
        (while (re-search-forward prot-org-cite-regexp nil t)
          (dotimes (n 4)
            (unless (= n 0)
              (when-let* ((beg (match-beginning n))
                          (end (match-end n))
                          (overlay (make-overlay beg end)))
                (overlay-put overlay 'invisible t)
                ;; NOTE: I am not using the `after-string' in this case,
                ;; but am adding here as it is a useful paradigm in
                ;; general.
                ;;
                ;; (overlay-put overlay 'after-string "")
                (overlay-put overlay 'prot-org-cite-overlay t)))))))))

(defun prot-org-cite-remove-overlays ()
  "Remove all `prot-org-cite-overlay' overlays from the current buffer."
  (when-let* ((overlays (save-restriction
                          (widen)
                          (overlays-in (point-min) (point-max))))
              (our-overlays (seq-filter
                             (lambda (overlay)
                               (overlay-get overlay 'prot-org-cite-overlay))
                             overlays)))
    (mapc #'delete-overlay our-overlays)))

(defface prot-org-cite '((t :inherit shadow))
  "Face for Org citations when `prot-org-cite-mode' is enabled.")

(defvar-local prot-org-cite-face-remap-object nil
  "Object of `face-remap-add-relative' for `prot-org-cite'.")

(defun prot-org-cite-remap-face (&optional unmap)
  "Remap the `org-cite-key' face to `prot-org-cite'.
With optional UNMAP, undo the remapping."
  (if unmap
      (progn
        (face-remap-remove-relative prot-org-cite-face-remap-object)
        (setq-local prot-org-cite-face-remap-object nil))
    (setq-local prot-org-cite-face-remap-object
                (face-remap-add-relative 'org-cite-key 'prot-org-cite))))

(define-minor-mode prot-org-cite-mode
  "Partially hide Org citations and style them with `prot-org-cite'.
More specifically, hide groups 1, 2, and 3 of `prot-org-cite-regexp'."
  :global nil
  (if prot-org-cite-mode
      (progn
        (prot-org-cite-add-overlays)
        (prot-org-cite-remap-face))
    (prot-org-cite-remove-overlays)
    (prot-org-cite-remap-face :unmap-the-face)))

Annotated version of prot-org-cite-mode

;; This defines the `prot-org-cite-regexp' variable.  Its value is a
;; regular expression (Emacs Lisp).  The official manual of Emacs
;; covers the technicalities.  You will notice that we number some the
;; groups.  They are the ones we care about.  The rest are ignored.
;;
;; We can use the command `re-builder' to test our Emacs Lisp regular
;; expression in the current buffer.
(defvar prot-org-cite-regexp
  "\\(?1:cite: ?\\)?\\(?2:@\\)\\(?:[a-z]+\\)\\(?3:.*?\\)\\(?:[0-9]\\{4,\\}\\)\\(?:[a-z]\\)?"
  "Regular expression matching an Org citation.
Groups 1, 2, and 3 are meant to be hidden when the minor mode
`prot-org-cite-mode' is enabled.")

;; Here we define a function that overlays.  An "overlay" is an object
;; that covers a certain region in the buffer.  A "region" is the
;; space between two buffer positions.  Overlays have a number of
;; uses.  The one we are interested in here is to make their regions
;; invisible, thus hiding the affected text.
(defun prot-org-cite-add-overlays ()
  "Add invisible overlays to `prot-org-cite-regexp' numbered groups."
  ;; For the purposes of this operation, we want to make sure that our
  ;; search is case-sensitive.  We thus `let' bind the `case-fold-search'
  ;; variable to a nil value.  Otherwise, our results will depend on the
  ;; user's configuration.
  (let ((case-fold-search nil))
    ;; Our code will have the side effect of changing the position of
    ;; the cursor (technically, the "point").  This is needed for our
    ;; purposes, but the user will ultimately want the changes to
    ;; happen without them losing their context.  The `save-excursion'
    ;; allows us to move the point and then trust that Emacs will
    ;; restore it to where it was before.
    (save-excursion
      ;; We want to operate in the entire buffer.  But the user may have
      ;; already narrowed to a portion thereof.  To respect their choice
      ;; while still doing the right thing, we have to wrap our code in
      ;; `save-restriction' and then `widen' the view.  This means that
      ;; our subsequent calls will run in the unnarrowed buffer and the
      ;; narrowing will be restored once we are done.
      (save-restriction
        (widen)
        ;; We start from the minimum visible position in the buffer.
        ;; Since we widened the view in the previous line of code, this
        ;; is the beginning of the buffer.  Otherwise, it would have
        ;; been the beginning of the narrowed portion of the buffer.
        (goto-char (point-min))
        ;; Starting from the top, we perform a search forward for the
        ;; `prot-org-cite-regexp'.  We do this in a loop.  The loop
        ;; works (i.e. is not infinite) because (i) the search has the
        ;; side effect of moving the point to the end of the match so
        ;; the search does not get stuck in one place, and (ii) we pass
        ;; the relevant argument to `re-search-forward' to return nil if
        ;; there is no match, instead of throwing an error.  The loop
        ;; only runs when its condition is non-nil.
        (while (re-search-forward prot-org-cite-regexp nil t)
          ;; Here I had to make a stylistic decision.  We want to add
          ;; overlays for the three numbered groups in our regular
          ;; expression.  I could have arranged to do this with another
          ;; `while', which would have its own local counter, but I
          ;; thought it would make the code a bit harder to read for our
          ;; purposes here.  So I am using `dotimes' instead, whose
          ;; semantics are in line with what we are doing, i.e. run
          ;; something N times.  The part about `dotimes' that I do not
          ;; like is that it starts from 0 and its maximum number is
          ;; exclusive, meaning that we only get up to 3 even though we
          ;; have the 4 there.  Someone who is not familiar with this
          ;; behaviour will thus find the following perplexing.
          (dotimes (n 4)
            ;; The group 0 in a regular expression is a special number
            ;; which refers to the entire match.  We do not want to do
            ;; anything with that.  We only care about numbers 1, 2, 3.
            ;; Thus, the first run of this `dotimes' does nothing other
            ;; than increment 0 to 1 and run again.
            (unless (= n 0)
              ;; Before we start adding our overlays, we need to be sure
              ;; that there is a match for the Nth group in our regular
              ;; expression.  If there is none, we skip it and move on
              ;; with our loop.  If there is a match, then we store its
              ;; beginning and end positions and then make an overlay
              ;; that stretches between those two.
              (when-let* ((beg (match-beginning n))
                          (end (match-end n))
                          (overlay (make-overlay beg end)))
                ;; Now that we have created our overlay, we are ready to
                ;; associated properties with it.  These are symbols
                ;; that may already have an internal meaning, such as
                ;; `invisible' or arbitrary symbols that we can use for
                ;; our own purposes later.  Each time we set a property,
                ;; we specify its value.  In principle, we could have a
                ;; fine-grained system with different values, though all
                ;; we need here is something that returns non-nil.
                (overlay-put overlay 'invisible t)
                ;; I keep the `after-string' overlay property here for
                ;; this demonstration.  We do not need it, though it is
                ;; how we can add an arbitrary string in the stead of
                ;; the text we made invisible (think of how Org folds
                ;; its headings, for example).
                ;;
                ;; (overlay-put overlay 'after-string "")

                ;; Finally, we want to make our overlays have a unique
                ;; property that allows us to identify them later.  I
                ;; have called this `prot-org-cite-overlay', though it
                ;; has no inherent meaning.  What matters is that it is
                ;; unambiguously ours and that it has a non-nil value.
                (overlay-put overlay 'prot-org-cite-overlay t)))))))))

;; This is the function which reverts `prot-org-cite-add-overlays'.
;; You will notice the same pattern of `save-restriction' for the
;; aforementioned reasons.
(defun prot-org-cite-remove-overlays ()
  "Remove all `prot-org-cite-overlay' overlays from the current buffer."
  (when-let* ((overlays (save-restriction
                          (widen)
                          (overlays-in (point-min) (point-max))))
              ;; Here we go through the whole list of overlays to
              ;; identify those which are ours.  Without this
              ;; filtering, we could/would cause something to break.
              (our-overlays (seq-filter
                             (lambda (overlay)
                               (overlay-get overlay 'prot-org-cite-overlay))
                             overlays)))
    ;; Now that we have found all our overlays, let us delete them.
    (mapc #'delete-overlay our-overlays)))

;; The default style for Org citations is like that of links.  In
;; general, this is fine though our mode is designed to tone down the
;; citations.  We thus define our custom face, which inherits from the
;; `shadow' face.  We will apply this in the buffer where `prot-org-cite-mode'
;; is enabled.
(defface prot-org-cite '((t :inherit shadow))
  "Face for Org citations when `prot-org-cite-mode' is enabled.")

;; The technicalities of how to apply a face instead of another are
;; covered by the function `face-remap-add-relative'.  Its return
;; value is a special object that we need to store, such that we can
;; remove it afterwards.  You will notice how this declaration of the
;; variable has the "local" specifier, meaning that `setq' is
;; functionally equivalent to `setq-local' (I still write the latter,
;; because I prefer the code to be more explicit about what it is
;; doing).
(defvar-local prot-org-cite-face-remap-object nil
  "Object of `face-remap-add-relative' for `prot-org-cite'.")

;; This function sets up the `face-remap-add-relative' that I
;; mentioned in the previous comment.  We could have defined a
;; separate function for undoing this effect, though I considered it
;; pertinent to demonstrate the use of optional parameters.  Anything
;; after the `&optional' defaults to nil if it is not supplied as an
;; argument.  So we can call the function without it, but we can also
;; furnish the argument if we need to.
;;
;; The UNMAP we define here does not treat its value specially.  Any
;; non-nil value will suffice.
(defun prot-org-cite-remap-face (&optional unmap)
  "Remap the `org-cite-key' face to `prot-org-cite'.
With optional UNMAP, undo the remapping."
  (if unmap
      (progn
        (face-remap-remove-relative prot-org-cite-face-remap-object)
        ;; As I noted before, the `defvar-local' makes it so that we do
        ;; not need to write `setq-local', as `setq' will do the same
        ;; thing.  I still prefer to be unambiguous.
        (setq-local prot-org-cite-face-remap-object nil))
    ;; In this call we see how the value of the variable
    ;; `prot-org-cite-face-remap-object' is set to the return value of
    ;; the `face-remap-add-relative' function call.  So we get the
    ;; side effect of the remap, while we also store the return value.
    (setq-local prot-org-cite-face-remap-object
                (face-remap-add-relative 'org-cite-key 'prot-org-cite))))

;; All we need to do now is put everything together.  Our minor mode
;; is, at its core, a simple toggle to do/undo our stylistic changes.
;; The :global keyword specifies whether our mode has effect across
;; all buffers, but ours is buffer-local because of the nil value we
;; specify.
(define-minor-mode prot-org-cite-mode
  "Partially hide Org citations and style them with `prot-org-cite'.
More specifically, hide groups 1, 2, and 3 of `prot-org-cite-regexp'."
  :global nil
  ;; What the `define-minor-mode' does behind the scenes is to define
  ;; an interactive function (a "command") and a variable that use the
  ;; same symbol, `prot-org-cite-mode' in this case.  There is no
  ;; clash, as Emacs has separate namespaces for functions and
  ;; variables.  Then, when we call the command it sets its
  ;; corresponding variable to a non-nil or nil value, so the rest of
  ;; our logic runs accordingly.
  (if prot-org-cite-mode
      ;; The `progn' is our way of saying "those multiple calls are a
      ;; bundle, so treat them as one".  This is necessary to write an
      ;; `if' statement (among others), because the THEN part has to
      ;; be one argument.  But the ELSE part does not need the `progn'
      ;; because of a special behaviour which automatically treats all
      ;; remaining arguments as a single list.  In Emacs Lisp
      ;; functions, this is achieved by the `&rest' keyword (well, the
      ;; `if' in particular does not use `&rest' because it is written
      ;; in C, though you will see that in plenty of places).
      (progn
        (prot-org-cite-add-overlays)
        (prot-org-cite-remap-face))
    (prot-org-cite-remove-overlays)
    ;; As I wrote in my commentary about the optional parameter of
    ;; `prot-org-cite-remap-face', it does not treat its value in any
    ;; special way.  What matters is for it to be non-nil.  In cases
    ;; like this one, I prefer to use a :KEYWORD, which can be any
    ;; arbitrary text: it always evaluates to itself, i.e. it is
    ;; non-nil (same for symbols, though a keyword in this context is
    ;; generally less ambiguous).
    (prot-org-cite-remap-face :unmap-the-face)))