home

My NixOS systems configurations.
Log | Files | Refs | LICENSE

commit f30a74ae51395c7c01494b0ea912a0111c94ce3a
parent ee865fb065c7b71b6a595414145d2bff1a8f3cd5
Author: Vincent Demeester <vincent@sbr.pm>
Date:   Mon, 20 Nov 2023 12:51:18 +0100

tools/emacs: some org-mode and denote configuration

Signed-off-by: Vincent Demeester <vincent@sbr.pm>

Diffstat:
Mtools/emacs/config/config-org.el | 47+++++++++++++++++++++++++++++++++++++++++------
Atools/emacs/lisp/denote-journal-extras.el | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/emacs/lisp/org-extra-emphasis.el | 805+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/emacs/lisp/org-protocol-capture-html.el | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1339 insertions(+), 6 deletions(-)

diff --git a/tools/emacs/config/config-org.el b/tools/emacs/config/config-org.el @@ -9,7 +9,7 @@ (defconst org-directory "~/desktop/org/" "org-mode directory, where most of the org-mode file lives") ;; P.A.R.A. setup -(defconst org-inbox-file (expand-file-name "inbox.org" org-directory) +(defconst org-inbox-file (expand-file-name "20231120T124316--inbox__inbox.org" org-directory) "New stuff collected in this file.") (defconst org-archive-dir (expand-file-name "archive" org-directory) @@ -19,7 +19,7 @@ "Project files directory.") (defconst org-projects-completed-dir (expand-file-name "projects" org-archive-dir) "Directory of completed project files.") -(defconst org-projects-future-file (expand-file-name "future.org" org-projects-dir) +(defconst org-projects-future-file (expand-file-name "20231120T124316--future-projects-incubation__project_future.org" org-projects-dir) "Future projects are collected in this file.") (defconst org-areas-dir (expand-file-name "areas" org-directory) @@ -48,6 +48,16 @@ (set-register ?a `(file . ,org-areas-dir)) (set-register ?r `(file . ,org-resources-dir)) +(defun vde/org-mode-hook () + "Org-mode hook" + (setq show-trailing-whitespace t) + (when (not (eq major-mode 'org-agenda-mode)) + (setq fill-column 90) + (auto-revert-mode) + (auto-fill-mode) + (org-indent-mode) + (add-hook 'before-save-hook #'save-and-update-includes nil 'make-it-local))) + (use-package org :mode (("\\.org$" . org-mode) ("\\.org.draft$" . org-mode)) @@ -57,8 +67,9 @@ ("C-c o a a" . org-agenda) ("C-c o s" . org-sort) ("<f12>" . org-agenda)) + :hook (org-mode . vde/org-mode-hook) :custom - (org-reverse-note-order '(("inbox.org" . t) ;; Insert items on top of inbox + (org-reverse-note-order '((org-inbox-file . t) ;; Insert items on top of inbox (".*" . nil))) ;; On any other file, insert at the bottom (org-archive-location (concat org-archive-dir "/%s::datetree/")) (org-agenda-file-regexp "^[a-zA-Z0-9-_]+.org$") @@ -90,7 +101,24 @@ :after org) (use-package org-tempo - :after (org)) + :after (org) + :custom + (org-structure-template-alist '(("a" . "aside") + ("c" . "center") + ("C" . "comment") + ("e" . "example") + ("E" . "export") + ("Ea" . "export ascii") + ("Eh" . "export html") + ("El" . "export latex") + ("q" . "quote") + ("s" . "src") + ("se" . "src emacs-lisp") + ("sE" . "src emacs-lisp :results value code :lexical t") + ("sg" . "src go") + ("sr" . "src rust") + ("sp" . "src python") + ("v" . "verse")))) ;; (use-package org-bullets ;; :if (not window-system) @@ -137,8 +165,15 @@ ;; Using denote as the "source" of my second brain *in* org-mode. (use-package denote - :commands (denote denote-date denote-link-or-create denote-open-or-create denote-signature) - :after org) + :after org + :custom + (denote-directory org-directory) + (denote-rename-buffer-format "📝 %t") + :config + (denote-rename-buffer-mode 1) + (require 'denote-journal-extras) + (setq denote-journal-extras-directory (expand-file-name "journal" org-directory) + denote-journal-extras-title-format 'day-date-month-year)) ;; (use-package org ;; ;; :ensure org-plus-contrib ;; load from the package instead of internal diff --git a/tools/emacs/lisp/denote-journal-extras.el b/tools/emacs/lisp/denote-journal-extras.el @@ -0,0 +1,213 @@ +;;; denote-journal-extras.el --- Convenience functions for daily journaling -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. + +;; Author: Protesilaos Stavrou <info@protesilaos.com> +;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht> +;; URL: https://git.sr.ht/~protesilaos/denote +;; Mailing-List: https://lists.sr.ht/~protesilaos/denote + +;; This file is NOT part of GNU Emacs. + +;; This program 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. + +;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This is a set of optional convenience functions that used to be +;; provided in the Denote manual. They facilitate the use of Denote +;; for daily journaling. + +;;; Code: + +(require 'denote) + +(defgroup denote-journal-extras nil + "Denote for daily journaling." + :group 'denote + :link '(info-link "(denote) Top") + :link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote")) + +(defcustom denote-journal-extras-directory + (expand-file-name "journal" denote-directory) + "Directory for storing daily journal entries. +This can either be the same as the variable `denote-directory' or +a subdirectory of it. + +A value of nil means to use the variable `denote-directory'. +Journal entries will thus be in a flat listing together with all +other notes. They can still be retrieved easily by searching for +the `denote-journal-extras-keyword'." + :group 'denote-journal-extras + :type '(choice (directory :tag "Provide directory path (is created if missing)") + (const :tag "Use the `denote-directory'" nil))) + +(defcustom denote-journal-extras-keyword "journal" + "Single word keyword to tag journal entries. +It is used by `denote-journal-extras-new-entry' to add a keyword +to the newly created file." + :group 'denote-journal-extras + :type 'string) + +(defcustom denote-journal-extras-title-format 'day-date-month-year-24h + "Date format to construct the title with `denote-journal-extras-new-entry'. +The value is either a symbol or an arbitrary string that is +passed to `format-time-string' (consult its documentation for the +technicalities). + +Acceptable symbols and their corresponding styles are: + +| Symbol | Style | +|-------------------------+-----------------------------------| +| day | Monday | +| day-date-month-year | Monday 19 September 2023 | +| day-date-month-year-24h | Monday 19 September 2023 20:49 | +| day-date-month-year-12h | Monday 19 September 2023 08:49 PM | + +With a nil value, make `denote-journal-extras-new-entry' prompt +for a title." + :group 'denote-journal-extras + :type '(choice + (const :tag "Prompt for title with `denote-journal-extras-new-entry'" nil) + (const :tag "Monday" + :doc "The `format-time-string' is: %A" + day) + (const :tag "Monday 19 September 2023" + :doc "The `format-time-string' is: %A %e %B %Y" + day-date-month-year) + (const :tag "Monday 19 September 2023 20:49" + :doc "The `format-time-string' is: %A %e %B %Y %H:%M" + day-date-month-year-24h) + (const :tag "Monday 19 September 2023 08:49 PM" + :doc "The `format-time-string' is: %A %e %B %Y %I:%M %^p" + day-date-month-year-12h) + (string :tag "Custom string with `format-time-string' specifiers"))) + +(defcustom denote-journal-extras-hook nil + "Normal hook called after `denote-journal-extras-new-entry'. +Use this to, for example, set a timer after starting a new +journal entry (refer to the `tmr' package on GNU ELPA)." + :group 'denote-journal-extras + :type 'hook) + +(defun denote-journal-extras-directory () + "Make the variable `denote-journal-extras-directory' and its parents." + (when-let (((stringp denote-journal-extras-directory)) + (directory (file-name-as-directory (expand-file-name denote-journal-extras-directory)))) + (when (not (file-directory-p denote-journal-extras-directory)) + (make-directory directory :parents)) + directory)) + +(defun denote-journal-extras-daily--title-format (&optional date) + "Return present date in `denote-journal-extras-title-format' or prompt for title. +With optional DATE, use it instead of the present date. DATE has +the same format as that returned by `current-time'." + (format-time-string + (if (and denote-journal-extras-title-format + (stringp denote-journal-extras-title-format)) + denote-journal-extras-title-format + (pcase denote-journal-extras-title-format + ('day "%A") + ('day-date-month-year "%A %e %B %Y") + ('day-date-month-year-24h "%A %e %B %Y %H:%M") + ('day-date-month-year-12h "%A %e %B %Y %I:%M %^p") + (_ (denote-title-prompt (format-time-string "%F" date))))) + date)) + +(defun denote-journal-extras--get-template () + "Return template that has `journal' key in `denote-templates'. +If no template with `journal' key exists but `denote-templates' +is non-nil, prompt the user for a template among +`denote-templates'. Else return nil. + +Also see `denote-journal-extras-new-entry'." + (if-let ((template (alist-get 'journal denote-templates))) + template + (when denote-templates + (denote-template-prompt)))) + +(defun denote-journal-extras--get-date (date) + "Return a valid DATE for `format-time-string'. +If DATE is a list, return it as-is. If it is a string, parse it +with `denote--valid-date'. Else return the `current-time'." + (cond + ((listp date) date) + ((stringp date) (denote--valid-date date)) + (t (current-time)))) + +;;;###autoload +(defun denote-journal-extras-new-entry (&optional date) + "Create a new journal entry in variable `denote-journal-extras-directory'. +Use `denote-journal-extras-keyword' as a keyword for the newly +created file. Set the title of the new entry according to the +value of the user option `denote-journal-extras-title-format'. + +With optional DATE as a prefix argument, prompt for a date. If +`denote-date-prompt-use-org-read-date' is non-nil, use the Org +date selection module. + +When called from Lisp DATE is a string and has the same format as +that covered in the documentation of the `denote' function. It +is internally processed by `denote-journal-extras--get-date'." + (interactive (list (when current-prefix-arg (denote-date-prompt)))) + (let ((internal-date (denote-journal-extras--get-date date)) + (denote-user-enforced-denote-directory (denote-journal-extras-directory))) + (denote + (denote-journal-extras-daily--title-format internal-date) + `(,denote-journal-extras-keyword) + nil nil date + (denote-journal-extras--get-template)) + (run-hooks 'denote-journal-extras-hook))) + +(defun denote-journal-extras--entry-today (&optional date) + "Return list of files matching a journal for today or optional DATE. +DATE has the same format as that returned by `denote-journal-extras--get-date'." + (denote-directory-files-matching-regexp + (format "%sT[0-9]\\{6\\}.*_%s" + (format-time-string "%Y%m%d" date) + denote-journal-extras-keyword))) + +;;;###autoload +(defun denote-journal-extras-new-or-existing-entry (&optional date) + "Locate an existing journal entry or create a new one. +A journal entry is one that has `denote-journal-extras-keyword' as +part of its file name. + +If there are multiple journal entries for the current date, +prompt for one using minibuffer completion. If there is only +one, visit it outright. If there is no journal entry, create one +by calling `denote-journal-extra-new-entry'. + +With optional DATE as a prefix argument, prompt for a date. If +`denote-date-prompt-use-org-read-date' is non-nil, use the Org +date selection module. + +When called from Lisp, DATE is a string and has the same format +as that covered in the documentation of the `denote' function. +It is internally processed by `denote-journal-extras--get-date'." + (interactive + (list + (when current-prefix-arg + (denote-date-prompt)))) + (let* ((internal-date (denote-journal-extras--get-date date)) + (files (denote-journal-extras--entry-today internal-date))) + (cond + ((length> files 1) + (find-file (completing-read "Select journal entry: " files nil :require-match))) + (files + (find-file (car files))) + (t + (denote-journal-extras-new-entry date))))) + +(provide 'denote-journal-extras) +;;; denote-journal-extras.el ends here diff --git a/tools/emacs/lisp/org-extra-emphasis.el b/tools/emacs/lisp/org-extra-emphasis.el @@ -0,0 +1,805 @@ +;;; org-extra-emphasis.el --- Extra Emphasis markers for Org -*- lexical-binding: t; coding: utf-8-emacs; -*- + +;; Copyright (C) 2022 Jambunathan K <kjambunathan at gmail dot com> +;; Copyright (C) 2004-2022 Free Software Foundation, Inc. + +;; Author: Jambunathan K <kjambunathan at gmail dot com> +;; Keywords: org +;; Homepage: https://github.com/kjambunathan/org-extra-emphasis +;; Version: 1.0 +;; Package-Requires: ((ox-odt "9.5.3.467")) + +;; This file is NOT part of GNU Emacs. + +;; This program 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. + +;; This program 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 <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Overview +;; ======== +;; +;; This library provides two additional markers `!!' and `!@' over +;; and above those in `org-emphasis-alist'.' +;; +;; - Text enclosed in `!!' is highlighted in yellow, and exported likewise +;; - Text enclosed in `!@' is displayed in red, and exported likewise +;; +;; Following backends are supported: HTML and ODT. For export of extra +;; emphasis markers to the ODT side, you need +;; [[https://github.com/kjambunathan/org-mode-ox-odt][Enhanced ODT]] +;; exporter with version >= 9.5.3.467 (dtd. June 14, 2022 IST). This +;; is the first version of the exporter that defines the user option +;; `org-odt-extra-styles'. +;; +;; Example +;; ======= +;; +;; Setup +;; ===== +;; +;; Add the following to your `user-init-file' and restart Emacs. +;; +;; (requrie 'org-extra-emphasis) +;; +;; Test Run +;; ======== +;; +;; 1. Create an `org' file, say `org-export-emphasis.org' and fill it +;; with following content or you can download the file from +;; https://raw.githubusercontent.com/kjambunathan/org-extra-emphasis/main/org-extra-emphasis.org + + ;; #+TITLE: Test file for ==org-extra-emphasis== library + + ;; * Demo of extra emphasis markers ==!!== and ==!@== + + ;; !!Ea consectetur laboris adipiscing et ipsum labore esse qui minim + ;; pariatur et sunt sunt nostrud anim laborum culpa.!! + + ;; !@Minim reprehenderit excepteur elit, dolore elit, veniam, eu. + ;; Ullamco dolore elit, cupidatat sed labore ea aute.!@ + + ;; Pariatur !!et lorem cupidatat !@minim irure!@ proident, ad.!! Eiusmod + ;; sunt et lorem labore ex aliqua aute esse. + + ;; Ut mollit !@duis velit est est magna in quis ipsum. !!Aliqua aliqua + ;; non laboris exercitation cupidatat aliqua incididunt.!! Qui voluptate + ;; irure aute occaecat laborum cillum est.!@ Quis magna dolor ullamco + ;; magna do consectetur est laborum enim ut. + + ;; * !!Demo of extra emphasis markers in a styled paragraph!! + + ;; #+ATTR_ODT: :target "extra_styles" + ;; #+begin_src nxml + ;; <style:style style:name="Warn" + ;; style:parent-style-name="Text_20_body" + ;; style:family="paragraph"> + ;; <style:paragraph-properties> + ;; <style:tab-stops /> + ;; </style:paragraph-properties> + ;; <style:text-properties fo:background-color="#ff0000" + ;; fo:color="#ffffff" + ;; fo:font-size="20pt" + ;; fo:font-style="italic" + ;; fo:font-weight="bold" /> + ;; </style:style> + ;; #+end_src + + ;; #+ATTR_ODT: :style "Warn" + ;; Proident, duis dolore consectetur sed nisi ea pariatur. Esse + ;; proident, cillum duis qui ullamco sint cillum magna. !!Eiusmod + ;; veniam, !@sint officia!@ non consectetur laboris cillum.!! Cillum + ;; mollit consequat eu dolore ullamco qui reprehenderit anim cillum + ;; in consectetur consequat sunt dolore aliquip voluptate + ;; consectetur anim ea. Voluptate nisi est incididunt aliquip + ;; excepteur aliqua id do enim ut non consequat. +;; +;; 2. Note that portions of text marked with `!!' and `!@' are fontified as described above. +;; +;; 3. Export the file to HTML with `C-c C-e h O'. +;; +;; Note that the text enclosed in the above emphasis markers are +;; colorized in HTML file. +;; +;; 4. Export the file to ODT with `C-c C-e o O'. +;; +;; Note that the text enclosed in the above emphasis markers are +;; colorized in ODT file. +;; +;; The HTML, ODT, PDF generated in steps (3) and (4) above are +;; available at https://github.com/kjambunathan/org-extra-emphasis and +;; the screenshots can be seen in https://github.com/kjambunathan/org-extra-emphasis/tree/main/screenshots +;; + +;; Default Settings +;; ================ +;; +;; 16 Emphasis Markers +;; =================== +;; +;; This library defines the following 16 emphasis markers, +;; +;; |----+----+----+----| +;; | !! | !@ | !% | !& | +;; |----+----+----+----| +;; | @! | @@ | @% | @& | +;; |----+----+----+----| +;; | %! | %@ | %% | %& | +;; |----+----+----+----| +;; | &! | &@ | &% | && | +;; |----+----+----+----| +;; +;; The above markers are all pairings of the following four characters: +;; ! @ % & +;; +;; It is hoped that these set of emphasis markers don't pose issues +;; while exporting. +;; +;; 17 Extra Emphasis Faces +;; ======================= +;; +;; This library defines 17 faces: +;; +;; - one base face `org-extra-emphasis' +;; - 16 more faces `org-extra-emphasis-01',`org-extra-emphasis-02', +;; ..., `org-extra-emphasis-16'. +;; +;; The later 16 faces derive from `org-extra-emphasis' face. Of +;; these, only the first two faces `org-extra-emphasis-01' and +;; `org-extra-emphasis-02' are explicitly configured. If you are +;; using more than 2 emphasis markers, you may want to configure the +;; other 14 faces. +;; +;; `org-extra-emphasis-alist' already associated 16 emphasis markers +;; with 16 different faces. +;; +;; Customization +;; ============= +;; +;; Configuring your own Emphasis Markers +;; ===================================== +;; +;; 16 numbers of emphasis markers should suffice in practice. +;; However, if none of the above emphasis markers resonate with you, +;; you can customize `org-extra-emphasis-alist', and plug in your own +;; markers. When choosing your own marker, ensure that you exercise +;; some care. For example, if you choose `#' as a marker you are +;; likely to get malformed `html' and `odt' files. +;; +;; Configuring Extra Emphasis Faces +;; =============================== +;; +;; You can use `M-x customize-group RET org-extra-emphasis-faces RET' +;; to configure the extra emphasis faces. +;; +;; Disabling the Extra Emphasis +;; ============================= +;; +;; You can use `M-x org-extra-emphasis-mode' to toggle this feature. +;; +;; Adding additional export backends +;; ================================= +;; +;; To add additional backends, modify `org-extra-emphasis-formatter' +;; and `org-extra-emphasis-build-backend-regexp'. + +;;; Code: + +(require 'org) +(require 'ox-odt) +(require 'rx) +(require 'htmlfontify) + +;;; PART-1: `org-extra-emphasis-mode' + +;;;; Internal Variables + +(defvar org-extra-emphasis-backends + '(html odt ods)) + +(defvar org-extra-emphasis-info + (list :enabled nil)) + +;; Helper snippets to convert a Emacs Face to Inine CSS and ODT Text Properties +;; +;; (defun org-extra-emphasis-emacs-face->inline-css (face) +;; (let ((s (cdr (hfy-face-to-css-default face)))) +;; (when (string-match (rx-to-string '(and "{" (group (zero-or-more any)) "}")) s) +;; (format "<span style=\"%s\">%%s</span>" (match-string 1 s))))) +;; +;; (org-extra-emphasis-emacs-face->inline-css 'hi-yellow) +;; (org-extra-emphasis-emacs-face->inline-css 'hi-red-b) +;; +;; (defun org-extra-emphasis-emacs-face->odt-text-properties (face) +;; (org-odt--lisp-to-xml +;; (assoc 'style:text-properties +;; (org-odt--xml-to-lisp +;; (cdr (org-odt-hfy-face-to-css face)))))) +;; +;; (org-extra-emphasis-emacs-face->odt-text-properties 'hi-yellow) +;; (org-extra-emphasis-emacs-face->odt-text-properties 'hi-red-b) + +(defun org-extra-emphasis-update (&rest _ignored) + "Workhorse function that responds to configuration changes. + +Current state is maintined in `org-extra-emphasis-info', a plist." + ;; When `org-extra-emaphasis' is ON, override use + ;; `org-extra-emphasis-org-do-emphasis-faces'. + ;; Otherwise, use `org-do-emphasis-faces'. + (cond + ((plist-get org-extra-emphasis-info :enabled) + (advice-add 'org-do-emphasis-faces :override + 'org-extra-emphasis-org-do-emphasis-faces)) + (t + (advice-remove 'org-do-emphasis-faces + 'org-extra-emphasis-org-do-emphasis-faces))) + ;; `org-extra-emphasis-alist' is effective only if + ;; `org-extra-emphasis' is enabled. + (plist-put org-extra-emphasis-info :work-alist + (when (plist-get org-extra-emphasis-info :enabled) + (plist-get org-extra-emphasis-info :alist))) + ;; Set properties that control fontification. + ;; The property names and their values mimics the corresponding + ;; variables in `org-set-emph-re'. + (plist-put org-extra-emphasis-info :org-emphasis-alist + (when (and (boundp 'org-emphasis-regexp-components) + org-emphasis-alist org-emphasis-regexp-components) + (append (plist-get org-extra-emphasis-info :work-alist) + org-emphasis-alist))) + (plist-put org-extra-emphasis-info :org-emph-re-template + (when (and (boundp 'org-emphasis-regexp-components) + org-emphasis-alist org-emphasis-regexp-components) + (pcase-let* + ((`(,pre ,post ,border ,body ,nl) org-emphasis-regexp-components) + (body (if (<= nl 0) body + (format "%s*?\\(?:\n%s*?\\)\\{0,%d\\}" body body nl)))) + (format (concat "\\([%s]\\|^\\)" ;before markers + "\\(\\(%%s\\)\\([^%s]\\|[^%s]%s[^%s]\\)\\3\\)" + "\\([%s]\\|$\\)") ;after markers + pre border border body border post)))) + (plist-put org-extra-emphasis-info :org-emph-re + (format (plist-get org-extra-emphasis-info :org-emph-re-template) + (rx-to-string + `(or ,@(mapcar #'car + (cl-remove-if (lambda (l) + (eq 'verbatim (nth 2 l))) + (plist-get org-extra-emphasis-info :org-emphasis-alist))))))) + (plist-put org-extra-emphasis-info :org-verbatim-re + (format (plist-get org-extra-emphasis-info :org-emph-re-template) + (rx-to-string + `(or ,@(mapcar #'car + (cl-remove-if-not (lambda (l) + (eq 'verbatim (nth 2 l))) + (plist-get org-extra-emphasis-info :org-emphasis-alist))))) + (rx-to-string + `(or ,@(mapcar #'car + (cl-remove-if-not (lambda (l) + (eq 'verbatim (nth 2 l))) + (plist-get org-extra-emphasis-info :org-emphasis-alist))))))) + ;; Set properties that control Export backends + ;; - Regexp to search for in the final exported document + (plist-put org-extra-emphasis-info :export-alist + (org-extra-emphasis-build-backend-regexp)) + + ;; - Generate ODT character styles for the extra emphasis faces and + ;; dump those in `org-odt-extra-styles' and `org-ods-automatic-styles'. + (plist-put org-extra-emphasis-info :odt-extra-styles + (let* ((odt-styles + (concat (mapconcat #'identity + (cl-loop for (_marker face) in (plist-get org-extra-emphasis-info :alist) + collect (cdr (org-odt-hfy-face-to-css face))) + "\n\n")))) + (with-no-warnings + (unless (boundp 'org-odt-extra-styles) + (message "`org-odt-extra-styles' not found. Upgrade to `ox-odt-9.5.3.467' or later.") + ;; (sleep-for 2) + (setq org-odt-extra-styles nil)) + (setq org-odt-extra-styles + (concat (or (when (boundp 'org-odt-extra-styles) + (get 'org-odt-extra-styles 'saved-value)) + "") + "\n\n" + odt-styles)) + (setq org-ods-automatic-styles + (concat (or (when (boundp 'org-ods-automatic-styles) + (get 'org-ods-automatic-styles 'saved-value)) + "") + "\n\n" + odt-styles)) + (message "`org-odt-extra-styles' and `org-ods-automatic-styles' is updated for this session") + ;; (sleep-for 1) + ) + odt-styles)) + ;; Re-fontify all Org buffers based on current configuration. + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (derived-mode-p 'org-mode) + (font-lock-flush))))) + +;;;; Fontify Extra Emphasis Markers + +(defun org-extra-emphasis-org-do-emphasis-faces (limit) + "Workhorse function that does fontification This function is +based on `org-do-emphasis-faces'. The property names and values +correspond to the variables used in `org-do-emphasis-faces'. Key +differences are: + + - `:org-emphasis-alist' includes entries for both standard + emphasis markers and extra emphasis markers. + + - The regexes used for search-based fontification allow for + the possibility that the emphasis markers _in all + likelihood_ are multi-char strings, as opposed to single + chars." + (let* ((quick-re (format "\\([%s]\\|^\\)\\(%s\\)" + (car org-emphasis-regexp-components) + (rx-to-string + `(or ,@(mapcar #'car (plist-get org-extra-emphasis-info :org-emphasis-alist))))))) + (catch :exit + (while (re-search-forward quick-re limit t) + (let* ((marker (match-string 2)) + (verbatim? (member marker '("~" "=")))) + (when (save-excursion + (goto-char (match-beginning 0)) + (and + ;; Do not match table hlines. + (not (and (equal marker "+") + (org-match-line + "[ \t]*\\(|[-+]+|?\\|\\+[-+]+\\+\\)[ \t]*$"))) + ;; Do not match headline stars. Do not consider + ;; stars of a headline as closing marker for bold + ;; markup either. + (not (and (equal marker "*") + (save-excursion + (forward-char) + (skip-chars-backward "*") + (looking-at-p org-outline-regexp-bol)))) + ;; Match full emphasis markup regexp. + (looking-at (if verbatim? (plist-get org-extra-emphasis-info :org-verbatim-re) + (plist-get org-extra-emphasis-info :org-emph-re))) + ;; Do not span over paragraph boundaries. + (not (string-match-p org-element-paragraph-separate + (match-string 2))) + ;; Do not span over cells in table rows. + (not (and (save-match-data (org-match-line "[ \t]*|")) + (string-match-p "|" (match-string 4)))))) + (pcase-let ((`(,_ ,face ,_) (assoc marker (plist-get org-extra-emphasis-info :org-emphasis-alist))) + (m (if org-hide-emphasis-markers 4 2))) + (font-lock-prepend-text-property + (match-beginning m) (match-end m) 'face face) + (when verbatim? + (org-remove-flyspell-overlays-in + (match-beginning 0) (match-end 0)) + (remove-text-properties (match-beginning 2) (match-end 2) + '(display t invisible t intangible t))) + (add-text-properties (match-beginning 2) (match-end 2) + '(font-lock-multiline t org-emphasis t)) + (when (and org-hide-emphasis-markers + (not (org-at-comment-p))) + (add-text-properties (match-end 4) (match-beginning 5) + '(invisible t)) + (add-text-properties (match-beginning 3) (match-end 3) + '(invisible t))) + (throw :exit t)))))))) + +;; There is no `:set' function for `deffaces'. So, when the extra +;; faces `org-extra-emphasis-01', `org-extra-emphasis-02' reconfigured, +;; we don't get a notification. The following export hook ensures +;; that `org-extra-emphasis-info' is in sync with user configuration. +(add-hook 'org-export-before-processing-hook 'org-extra-emphasis-update) + +;;;; Export Extra Emphasis Markers + +(defun org-extra-emphasis-formatter (marker text backend) + "Style TEXT in the same font face as the face MARKER is mapped to. +Note that TEXT is in BACKEND format. + +This currently supports HTML and ODT backends. + +See `org-extra-emphasis-alist' for MARKER to face mappings." + (let* ((face (car (assoc-default marker (plist-get org-extra-emphasis-info :work-alist)))) + (encode-attribute-value + (lambda (text) + (dolist (pair '(("&" . "&amp;") + ("<" . "&lt;") + (">" . "&gt;") + ("'" . "&apos;") + ("\"" . "&quot;"))) + (setq text (replace-regexp-in-string (car pair) (cdr pair) text t t))) + text))) + (cl-case backend + ((odt ods) + (format "<text:span text:style-name=\"%s\">%s</text:span>" + (car (org-odt-hfy-face-to-css face)) text)) + (html + (format "<span class=\"%s\" style=\"%s\">%s</span>" + face + ;; An alternate implementation of + ;; `hfy-face-to-css-default' which performs correctly + ;; when a face specifies a `:family', and/or inherits + ;; some attributes from other faces. Note that the + ;; flattening (or non-duplication) of face attributes + ;; here is done by Emacs itself. + (mapconcat (lambda (x) + (when (cdr x) + (format "%s: %s;" (car x) + (funcall encode-attribute-value (cdr x))))) + (hfy-face-to-style-i + (cl-loop with props = (mapcar #'car face-attribute-name-alist) + for prop in props + for value = (face-attribute face prop nil 'default) + unless (eq prop :inherit) + append (list prop value))) + " ") + text)) + (_ text)))) + +(defun org-extra-emphasis-build-backend-regexp () + "Regexp to search for emphasized text in exported file. +This function transcode an emphasis MARKER which is in plain text +format, to the BACKEND format. That is, if you use `<<' as an +emphasis marker, you need to search for `&lt;&lt;' in the +exported HTML file. + +See `org-extra-emphasis-alist' for more information" + (cl-loop for (marker . spec) in (plist-get org-extra-emphasis-info :work-alist) collect + (cons marker + (cl-loop for backend in org-extra-emphasis-backends collect + (cons backend + (rx-to-string `(and ,(org-export-data-with-backend marker backend nil) + (group (minimal-match + (zero-or-more (or any "\n")))) + ,(org-export-data-with-backend marker backend nil)))))))) + +(defun org-extra-emphasis-plain-text-filter (text backend _info) + "Transcode TEXT in to BACKEND format. +Uses `org-extra-emphasis-formatter' to do the transcoding. + +Search TEXT for one or more transcoded MARKERs, and mark it up as +specified in `org-extra-emphasis-alist'." + (with-temp-buffer + (insert text) + (cl-loop for (marker . spec) in (plist-get org-extra-emphasis-info :export-alist) + for regex = (assoc-default backend spec) + do (goto-char (point-min)) + (if (not regex) text + (while (re-search-forward regex nil t) + (let* ((contents (match-string 1)) + (emphasized-contents (save-match-data + (org-extra-emphasis-formatter + marker contents backend)))) + (replace-match emphasized-contents t t))))) + (buffer-substring-no-properties (point-min) (point-max)))) + +;; Install export filter for transcoding extra emphasis markers. +(defun org-extra-emphasis-update-filter-functions (&optional export-filter-functions) + (let* ((all-filter-functions (thread-last org-export-filters-alist + (seq-map #'cdr) + (seq-sort #'string<)))) + (dolist (filter-fn '(org-extra-emphasis-plain-text-filter org-extra-emphasis-strip-zws-maybe)) + (dolist (it all-filter-functions) + (set it (delq filter-fn (symbol-value it)))) + (dolist (it export-filter-functions) + (add-to-list it filter-fn))))) + +;;;; User Options & Commands + +;;;;; Custom Groups + +(defgroup org-extra-emphasis nil + "Options for highlighting and exporting extra emphasis markers in Org files." + :tag "Org Extra Emphasis" + :group 'org) + +(defgroup org-extra-emphasis-faces nil + "Faces for Org Extra Emphasis." + :group 'org-extra-emphasis + :group 'faces) + +;;;; Custom Faces + +(defface org-extra-emphasis nil + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-01 + '((t (:inherit org-extra-emphasis :background "yellow"))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-02 + '((t (:inherit org-extra-emphasis :foreground "red"))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-03 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-04 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-05 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-06 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-07 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-08 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-09 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-10 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-11 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-12 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-13 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-14 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-15 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +(defface org-extra-emphasis-16 + '((t (:inherit org-extra-emphasis))) + "A face for Org Extra Emphasis." + :group 'org-extra-emphasis-faces) + +;;;;; Useful Org Setting + +(setcar (last org-emphasis-regexp-components) 5) + +(defcustom org-extra-emphasis-alist + '(("!!" org-extra-emphasis-01) + ("!@" org-extra-emphasis-02) + ("!%" org-extra-emphasis-03) + ("!&" org-extra-emphasis-04) + ("@!" org-extra-emphasis-05) + ("@@" org-extra-emphasis-06) + ("@%" org-extra-emphasis-07) + ("@&" org-extra-emphasis-08) + ("%!" org-extra-emphasis-09) + ("%@" org-extra-emphasis-10) + ("%%" org-extra-emphasis-11) + ("%&" org-extra-emphasis-12) + ("&!" org-extra-emphasis-13) + ("&@" org-extra-emphasis-14) + ("&%" org-extra-emphasis-15) + ("&&" org-extra-emphasis-16)) + "Alist of emphasis marker and its associated face." + :group 'org-extra-emphasis + :type '(repeat + (list + (string :tag "Emphasis Marker") + (face :tag "Face"))) + :set (lambda (var val) + (set var val) + (plist-put org-extra-emphasis-info :alist val) + (org-extra-emphasis-update))) + +(defcustom org-extra-emphasis t + "When non-nil, enable Org Extra Emphasis." + :group 'org-extra-emphasis + :type '(boolean "Org Extra Emphasis") + :set (lambda (var val) + (set var val) + (plist-put org-extra-emphasis-info :enabled val) + (org-extra-emphasis-update))) + +(defcustom org-extra-emphasis-filter-functions + '( + org-export-filter-headline-functions + org-export-filter-paragraph-functions + org-export-filter-table-cell-functions + ) + "List of places to which `org-extra-emphasis-plain-text-filter' +and `org-extra-emphasis-strip-zws-maybe' hooks itself. + +The places should be one among the values that occur in +`org-export-filters-alist'. + +By default, the list includes + - `org-export-filter-headline-functions' + - `org-export-filter-paragraph-functions' + - `org-export-filter-table-cell-functions', + +This means that text with extra emphasis which appears as plain +text, or within headlines and table cells will be, fontified." + :group 'org-extra-emphasis + :type `(set + ,@(thread-last org-export-filters-alist + (seq-map #'cdr) + (seq-sort #'string<) + (seq-map (lambda (it) + (list 'const it))))) + :set (lambda (var value) + (set-default var value) + (org-extra-emphasis-update-filter-functions value))) + +;;;;; `M-x org-extra-emphasis-mode' + +(defun org-extra-emphasis-mode (&optional arg) + "Enable / Disable Org Extra Emphasis. + +If called interactively, toggle Extra Emphasis. + +When called non-interactively, enable Extra Emphasis if ARG is +positive; disable otherwise." + (interactive "p") + (cond + ;; Called interactively; Toggle + ((called-interactively-p 'any) + (setq org-extra-emphasis (not org-extra-emphasis))) + ;; Called programatically; enable if arg >= 1 + ((and (numberp arg) + (>= arg 1)) + (setq org-extra-emphasis t)) + ;; Otherwise, disable + (t + (setq org-extra-emphasis nil))) + (plist-put org-extra-emphasis-info :enabled org-extra-emphasis) + (org-extra-emphasis-update)) + +;;; PART-2: `org-extra-emphasis-intraword-emphasis-mode' + +;;;; User options + +(defface org-extra-emphasis-zws-face + '((t (:inherit org-extra-emphasis :foreground "red"))) + "Use this face to highlight the ZERO WIDTH SPACE character." + :group 'org-extra-emphasis-faces) + +(defcustom org-extra-emphasis-zws-display-char ?\N{SPACING UNDERSCORE} + "Use the glyph of this character to display ZERO WIDTH SPACE. + +Set this to nil, if you want the ZERO WIDTH SPACE to remain +inconspicuous in the buffer. Note that even if ZERO WIDTH SPACE +is inconspicuos in the buffer, the ZERO WIDTH SPACE will be +stripped from the export output accoding to the value of +`org-extra-emphasis-intraword-emphasis-mode'." + :type '(choice (const :tag "Disabled" nil) + (character :tag "Display ZERO WIDTH SPACE as ")) + :group 'org-extra-emphasis) + +;;;; Internal Variables + +(defvar-local org-extra-emphasis-stashed-display-table nil + "Stashed value of `buffer-display-table'. + +This is the value of `buffer-display-table' before +`org-extra-emphasis-intraword-emphasis-mode' is turned on in the +buffer. + +Use this value to restore a buffer's `buffer-display-table' when +`org-extra-emphasis-intraword-emphasis-mode' is turned off in the +buffer.") + +;;;; `M-x org-extra-emphasis-intraword-emphasis-mode' + +;;;###autoload +(define-minor-mode org-extra-emphasis-intraword-emphasis-mode + "Toggle intra word emphasis in `org-mode' export. + +When `org-extra-emphasis-intraword-emphasis-mode' is enabled: + +- ZERO WIDTH SPACE characters are stripped from export backends. +- ZERO WIDTH SPACE characters are displayed using + `org-extra-emphasis-zws-display-char' and highlighted with + `org-extra-emphasis-zws-face' space. + +TIPS for the user: + +1. You can insert ZERO WIDTH SPACE using + + `M-x insert-char RET ZERO WIDTH SPACE RET' + + One another way is to store that the ZERO WIDTH SPACE in a + register, say SPC, and + + (set-register ?\N{SPACE} \"\N{ZERO WIDTH SPACE}\") + + and use the \\[insert-register] command on that register to insert + the ZERO WIDTH SPACE character. + +2. You can examine the presence of ZERO WIDTH SPACE character in the + export output by turning on the `glyphless-display-mode'." + :lighter " ZWS" + :init-value nil + :global t + :group 'org-extra-emphasis + (cond + ;; Turn ON `org-extra-emphasis-intraword-emphasis-mode' + (org-extra-emphasis-intraword-emphasis-mode + (when org-extra-emphasis-zws-display-char + ;; Display ZERO WIDTH CHAR in a conspicuous way. + (setq org-extra-emphasis-stashed-display-table (copy-sequence buffer-display-table)) + (unless buffer-display-table + (setq buffer-display-table (make-display-table))) + (aset buffer-display-table + ?\N{ZERO WIDTH SPACE} + (vector (make-glyph-code org-extra-emphasis-zws-display-char + 'org-extra-emphasis-zws-face))))) + (t + ;; Turn OFF `org-extra-emphasis-intraword-emphasis-mode' + (when org-extra-emphasis-zws-display-char + ;; Restore the buffer's original `buffer-display-table'. + (setq buffer-display-table org-extra-emphasis-stashed-display-table))))) + +;; Adjust `buffer-display-table' so that ZERO WIDTH SPACE characters +;; are displayed. +(add-hook 'org-mode-hook 'org-extra-emphasis-intraword-emphasis-mode t) + +;;;; Export hook to strip ZERO WIDTH SPACE + +(defun org-extra-emphasis-strip-zws-maybe (text _backend _info) + "Strip ZERO WIDTH SPACE from TEXT. + +If `org-extra-emphasis-intraword-emphasis-mode' is enabled, strip +ZERO WIDTH SPACE from TEXT. Otherwise, return TEXT unmodified." + (cond + ;; `org-extra-emphasis-intraword-emphasis-mode' is ON + (org-extra-emphasis-intraword-emphasis-mode + ;; Strip ZERO WIDTH SPACE. + (replace-regexp-in-string + (rx-to-string `(one-or-more ,(char-to-string ?\N{ZERO WIDTH SPACE}))) + "" text t t)) + ;; `org-extra-emphasis-intraword-emphasis-mode' is OFF. + (t + ;; Nothing to do. + text))) + +;; Configure Org Export Engine to strip ZERO WIDTH SPACE, if needed. +;; (dolist (it '(org-export-filter-table-cell-functions +;; org-export-filter-paragraph-functions)) +;; (add-to-list it 'org-extra-emphasis-strip-zws-maybe it)) + +(provide 'org-extra-emphasis) + +;;; org-extra-emphasis.el ends here diff --git a/tools/emacs/lisp/org-protocol-capture-html.el b/tools/emacs/lisp/org-protocol-capture-html.el @@ -0,0 +1,280 @@ +;;; org-protocol-capture-html.el --- Capture HTML with org-protocol + +;; URL: https://github.com/alphapapa/org-protocol-capture-html +;; Version: 0.1-pre +;; Package-Requires: ((emacs "24.4")) + +;;; Commentary: + +;; This package captures Web pages into Org-mode using Pandoc to +;; process HTML. It can also use eww's eww-readable functionality to +;; get the main content of a page. + +;; These are the helper functions that run in Emacs. To capture pages +;; into Emacs, you can use either a browser bookmarklet or the +;; org-protocol-capture-html.sh shell script. See the README.org file +;; for instructions. + +;;; License: + +;; This program 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. + +;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Code: + +;;;; Require + +(require 'org-protocol) +(require 'cl-lib) +(require 'subr-x) +(require 's) + +;;;; Vars + +(defcustom org-protocol-capture-html-demote-times 1 + "How many times to demote headings in captured pages. +You may want to increase this if you use a sub-heading in your capture template." + :group 'org-protocol-capture-html :type 'integer) + +;;;; Test Pandoc + +(defconst org-protocol-capture-html-pandoc-no-wrap-option nil + ;; Set this so it won't be unbound + "Option to pass to Pandoc to disable wrapping. +Pandoc >= 1.16 deprecates `--no-wrap' in favor of +`--wrap=none'.") + +(defun org-protocol-capture-html--define-pandoc-wrap-const () + "Set `org-protocol-capture-html-pandoc-no-wrap-option'." + (setq org-protocol-capture-html-pandoc-no-wrap-option + ;; Pandoc >= 1.16 deprecates the --no-wrap option, replacing it with + ;; --wrap=none. Sending the wrong option causes output to STDERR, + ;; which `call-process-region' doesn't like. So we test Pandoc to see + ;; which option to use. + (with-temp-buffer + (let* ((process (start-process "test-pandoc" (current-buffer) "pandoc" "--dump-args" "--no-wrap")) + (limit 3) + (checked 0)) + (while (process-live-p process) + (if (= checked limit) + (progn + ;; Pandoc didn't exit in time. Kill it and raise + ;; an error. This function will return `nil' and + ;; `org-protocol-capture-html-pandoc-no-wrap-option' + ;; will remain `nil', which will cause this + ;; function to run again and set the const when a + ;; capture is run. + (set-process-query-on-exit-flag process nil) + (error "Unable to test Pandoc! Please report this bug! (include the output of \"pandoc --dump-args --no-wrap\")")) + (sleep-for 0.2) + (cl-incf checked))) + (if (and (zerop (process-exit-status process)) + (not (string-match "--no-wrap is deprecated" (buffer-string)))) + "--no-wrap" + "--wrap=none"))))) + +;;;; Direct-to-Pandoc + +(defun org-protocol-capture-html--with-pandoc (data) + "Process an org-protocol://capture-html:// URL using DATA. + +This function is basically a copy of `org-protocol-do-capture', but +it passes the captured content (not the URL or title) through +Pandoc, converting HTML to Org-mode." + + ;; It would be nice to not basically duplicate + ;; `org-protocol-do-capture', but passing the data back to that + ;; function would require re-encoding the data into a URL string + ;; with Emacs after Pandoc converts it. Since we've already split + ;; it up, we might as well go ahead and run the capture directly. + + (unless org-protocol-capture-html-pandoc-no-wrap-option + (org-protocol-capture-html--define-pandoc-wrap-const)) + + (let* ((template (or (plist-get data :template) + org-protocol-default-template-key)) + (url (org-protocol-sanitize-uri (plist-get data :url))) + (type (if (string-match "^\\([a-z]+\\):" url) + (match-string 1 url))) + (title (or (org-protocol-capture-html--nbsp-to-space (string-trim (plist-get data :title))) "")) + (content (or (org-protocol-capture-html--nbsp-to-space (string-trim (plist-get data :body))) "")) + (orglink (org-make-link-string + url (if (string-match "[^[:space:]]" title) title url))) + (org-capture-link-is-already-stored t)) ; avoid call to org-store-link + + (setq org-stored-links + (cons (list url title) org-stored-links)) + (kill-new orglink) + + (with-temp-buffer + (insert content) + (if (not (zerop (call-process-region + (point-min) (point-max) + "pandoc" t t nil "-f" "html" "-t" "org" org-protocol-capture-html-pandoc-no-wrap-option))) + (message "Pandoc failed: %s" (buffer-string)) + (progn + ;; Pandoc succeeded + (org-store-link-props :type type + :annotation orglink + :link url + :description title + :orglink orglink + :initial (buffer-string))))) + (org-protocol-capture-html--do-capture) + nil)) + +(add-to-list 'org-protocol-protocol-alist + '("capture-html" + :protocol "capture-html" + :function org-protocol-capture-html--with-pandoc + :kill-client t)) + +;;;; eww-readable + +(defvar url-http-end-of-headers) + +(eval-when-compile + ;; eww-readable only works on Emacs >=25.1, but I think it's better + ;; to check for the actual symbols. I think using + ;; `eval-when-compile' is the right way to do this, but I'm not + ;; sure. + (when (and (require 'eww nil t) + (require 'dom nil t) + (fboundp 'eww-score-readability)) + + (defun org-protocol-capture-html--capture-eww-readable (data) + "Capture content of URL with eww-readable.." + + (unless org-protocol-capture-html-pandoc-no-wrap-option + (org-protocol-capture-html--define-pandoc-wrap-const)) + + (let* ((template (or (plist-get data :template) + org-protocol-default-template-key)) + (url (org-protocol-sanitize-uri (plist-get data :url))) + (type (if (string-match "^\\([a-z]+\\):" url) + (match-string 1 url))) + (html (org-protocol-capture-html--url-html url)) + (result (org-protocol-capture-html--eww-readable html)) + (title (cdr result)) + (content (with-temp-buffer + (insert (org-protocol-capture-html--nbsp-to-space (car result))) + ;; Convert to Org with Pandoc + (unless (= 0 (call-process-region (point-min) (point-max) + "pandoc" t t nil "-f" "html" "-t" "org" + org-protocol-capture-html-pandoc-no-wrap-option)) + (error "Pandoc failed")) + (save-excursion + ;; Remove DOS CR/LF line endings + (goto-char (point-min)) + (while (search-forward (string ?\C-m) nil t) + (replace-match ""))) + ;; Demote page headings in capture buffer to below the + ;; top-level Org heading and "Article" 2nd-level heading + (save-excursion + (goto-char (point-min)) + (while (re-search-forward (rx bol (1+ "*") (1+ space)) nil t) + (beginning-of-line) + (insert "**") + (end-of-line))) + (buffer-string))) + (orglink (org-make-link-string + url (if (s-present? title) title url))) + ;; Avoid call to org-store-link + (org-capture-link-is-already-stored t)) + + (setq org-stored-links + (cons (list url title) org-stored-links)) + (kill-new orglink) + + (org-store-link-props :type type + :annotation orglink + :link url + :description title + :orglink orglink + :initial content) + (org-protocol-capture-html--do-capture) + nil)) + + (add-to-list 'org-protocol-protocol-alist + '("capture-eww-readable" + :protocol "capture-eww-readable" + :function org-protocol-capture-html--capture-eww-readable + :kill-client t)) + + (defun org-protocol-capture-html--url-html (url) + "Return HTML from URL as string." + (let* ((response-buffer (url-retrieve-synchronously url nil t)) + (encoded-html (with-current-buffer response-buffer + (pop-to-buffer response-buffer) + ;; Skip HTTP headers, using marker provided by url-http + (delete-region (point-min) (1+ url-http-end-of-headers)) + (buffer-string)))) + (kill-buffer response-buffer) ; Not sure if necessary to avoid leaking buffer + (with-temp-buffer + ;; For some reason, running `decode-coding-region' in the + ;; response buffer has no effect, so we have to do it in a + ;; temp buffer. + (insert encoded-html) + (condition-case nil + ;; Fix undecoded text + (decode-coding-region (point-min) (point-max) 'utf-8) + (coding-system-error nil)) + (buffer-string)))) + + (defun org-protocol-capture-html--eww-readable (html) + "Return `eww-readable' part of HTML with title. +Returns list (HTML . TITLE)." + ;; Based on `eww-readable' + (let* ((html + ;; Convert "&nbsp;" in HTML to plain spaces. + ;; `libxml-parse-html-region' turns them into + ;; underlines. The closest I can find to an explanation + ;; is at <http://www.perlmonks.org/?node_id=825188>. + (org-protocol-capture-html--nbsp-to-space html)) + (dom (with-temp-buffer + (insert html) + (libxml-parse-html-region (point-min) (point-max)))) + (title (cl-caddr (car (dom-by-tag dom 'title))))) + (eww-score-readability dom) + (cons (with-temp-buffer + (shr-dom-print (eww-highest-readability dom)) + (buffer-string)) + title))))) + +;;;; Helper functions + +(defun org-protocol-capture-html--nbsp-to-space (s) + "Convert HTML non-breaking spaces to plain spaces in S." + ;; Not sure why sometimes these are in the HTML and Pandoc converts + ;; them to underlines instead of spaces, but this fixes it. + (replace-regexp-in-string (rx "&nbsp;") " " s t t)) + +(with-no-warnings + ;; Ignore warning about the dynamically scoped `template' variable. + (defun org-protocol-capture-html--do-capture () + "Call `org-capture' and demote page headings in capture buffer." + (raise-frame) + (funcall 'org-capture nil template) + + ;; Demote page headings in capture buffer to below the + ;; top-level Org heading + (save-excursion + (goto-char (point-min)) + (re-search-forward (rx bol "*" (1+ space)) nil t) ; Skip 1st heading + (while (re-search-forward (rx bol "*" (1+ space)) nil t) + (dotimes (n org-protocol-capture-html-demote-times) + (org-demote-subtree)))))) + +(provide 'org-protocol-capture-html) + +;;; org-protocol-capture-html.el ends here