From: Andreas Gerler Date: Wed, 21 May 2025 12:02:00 +0000 (+0200) Subject: update elfeed-org X-Git-Url: http://git.bundesbrandschatzamt.de/?a=commitdiff_plain;h=a079c5c92382c203640be1f17a7cd8402541df2f;p=emacsconfig.git update elfeed-org --- diff --git a/bba.org b/bba.org index ffec4ae6..8cce2bfc 100644 --- a/bba.org +++ b/bba.org @@ -1133,7 +1133,7 @@ necessary. (if (>= emacs-major-version 24) (progn (add-to-list 'load-path "~/.emacs.d/elpa/elfeed-20210822.2129") - (add-to-list 'load-path "~/.emacs.d/elpa/elfeed-org-20221118.852") + (add-to-list 'load-path "~/.emacs.d/elpa/elfeed-org-20250219.950") (require 'elfeed-org) (elfeed-org) (setq rmh-elfeed-org-files (list "~/org/elfeed.org")))) diff --git a/elpa/elfeed-org-20231009.1125/elfeed-org-autoloads.el b/elpa/elfeed-org-20231009.1125/elfeed-org-autoloads.el deleted file mode 100644 index ac9aaa0b..00000000 --- a/elpa/elfeed-org-20231009.1125/elfeed-org-autoloads.el +++ /dev/null @@ -1,30 +0,0 @@ -;;; elfeed-org-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- -;; Generated by the `loaddefs-generate' function. - -;; This file is part of GNU Emacs. - -;;; Code: - -(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) - - - -;;; Generated autoloads from elfeed-org.el - -(autoload 'elfeed-org "elfeed-org" "\ -Hook up rmh-elfeed-org to read the `org-mode' configuration when elfeed is run." t) -(register-definition-prefixes "elfeed-org" '("elfeed-org-" "rmh-elfeed-")) - -;;; End of scraped data - -(provide 'elfeed-org-autoloads) - -;; Local Variables: -;; version-control: never -;; no-byte-compile: t -;; no-update-autoloads: t -;; no-native-compile: t -;; coding: utf-8-emacs-unix -;; End: - -;;; elfeed-org-autoloads.el ends here diff --git a/elpa/elfeed-org-20231009.1125/elfeed-org-pkg.el b/elpa/elfeed-org-20231009.1125/elfeed-org-pkg.el deleted file mode 100644 index 9b6069aa..00000000 --- a/elpa/elfeed-org-20231009.1125/elfeed-org-pkg.el +++ /dev/null @@ -1,12 +0,0 @@ -;; -*- no-byte-compile: t; lexical-binding: nil -*- -(define-package "elfeed-org" "20231009.1125" - "Configure elfeed with one or more org-mode files." - '((elfeed "1.1.1") - (org "8.2.7") - (cl-lib "0.5")) - :url "https://github.com/remyhonig/elfeed-org" - :commit "fe59a96969bd321f5f9ec7317a4bc80943b94c86" - :revdesc "fe59a96969bd" - :keywords '("news") - :authors '(("Remy Honig" . "remyhonig@gmail.com")) - :maintainers '(("Remy Honig" . "remyhonig@gmail.com"))) diff --git a/elpa/elfeed-org-20231009.1125/elfeed-org.el b/elpa/elfeed-org-20231009.1125/elfeed-org.el deleted file mode 100644 index 6bd1af10..00000000 --- a/elpa/elfeed-org-20231009.1125/elfeed-org.el +++ /dev/null @@ -1,448 +0,0 @@ -;;; elfeed-org.el --- Configure elfeed with one or more org-mode files -*- lexical-binding: t; -*- - -;; Copyright (C) 2014 Remy Honig - -;; Author : Remy Honig -;; Package-Requires : ((elfeed "1.1.1") (org "8.2.7") (cl-lib "0.5")) -;; URL : https://github.com/remyhonig/elfeed-org -;; Version : 20170423.1 -;; Keywords : news - -;; 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 . - -;; Package-Version: 20231009.1125 -;; Package-Revision: fe59a96969bd - -;;; Commentary: -;; Maintaining tags for all rss feeds is cumbersome using the regular -;; flat list where there is no hierarchy and tag names are duplicated -;; a lot. Org-mode makes the book keeping of tags and feeds much -;; easier. Tags get inherited from parent headlines. Multiple files -;; can be specified to separate your private from your work feeds for -;; example. You may also use tagging rules to tag feeds by entry-title -;; keywords. See https://github.com/remyhonig/elfeed-org for usage. - -;;; Code: - -(require 'elfeed) -(require 'org) -(require 'org-element) -(require 'cl-lib) -(require 'xml) - - -(defgroup elfeed-org nil - "Configure the Elfeed RSS reader with an Orgmode file" - :group 'comm) - - -(defcustom rmh-elfeed-org-tree-id "elfeed" - "The tag or ID property on the trees containing the RSS feeds." - :group 'elfeed-org - :type 'string) - -(defcustom rmh-elfeed-org-ignore-tag "ignore" - "The tag on the feed trees that will be ignored." - :group 'elfeed-org - :type 'string) - -(defcustom rmh-elfeed-org-auto-ignore-invalid-feeds nil - "Tag feeds to ignore them when a feed could not loaded." - :group 'elfeed-org - :type 'bool) - -(defcustom rmh-elfeed-org-files (list (locate-user-emacs-file "elfeed.org")) - "The files where we look to find trees with the `rmh-elfeed-org-tree-id'. -In this file paths can be given relative to `org-directory'." - :group 'elfeed-org - :type '(repeat (file :tag "org-mode file"))) - -(defvar elfeed-org-new-entry-hook nil - "List of new-entry tagger hooks created by elfeed-org.") - -(defun rmh-elfeed-org-check-configuration-file (file) - "Make sure FILE exists." - (when (not (file-exists-p (expand-file-name file org-directory))) - (error "Elfeed-org cannot open %s. Make sure it exists or customize the variable \'rmh-elfeed-org-files\'" - (abbreviate-file-name file)))) - -(defun rmh-elfeed-org-is-headline-contained-in-elfeed-tree () - "Is any ancestor a headline with the elfeed tree id. -Return t if it does or nil if it does not." - (let ((result nil)) - (save-excursion - (while (and (not result) (org-up-heading-safe)) - (setq result (member rmh-elfeed-org-tree-id (org-get-tags)))) - result))) - -(defun rmh-elfeed-org-mark-feed-ignore (url) - "Set tag `rmh-elfeed-org-ignore-tag' to headlines containing the feed URL." - (dolist (org-file rmh-elfeed-org-files) - (with-temp-buffer - (insert-file-contents org-file) - (let ((org-inhibit-startup t) - (org-mode-hook nil)) - (org-mode)) - (goto-char (point-min)) - (while (and - (search-forward url nil t) - (org-at-heading-p t) - (rmh-elfeed-org-is-headline-contained-in-elfeed-tree)) - (org-toggle-tag rmh-elfeed-org-ignore-tag 'on)) - (elfeed-log 'info "elfeed-org tagged '%s' in file '%s' with '%s' to be ignored" url org-file rmh-elfeed-org-ignore-tag)))) - - -(defun rmh-elfeed-org-import-trees (tree-id) - "Get trees with \":ID:\" property or tag of value TREE-ID. -Return trees with TREE-ID as the value of the id property or -with a tag of the same value. Setting an \":ID:\" property is not -recommended but I support it for backward compatibility of -current users." - (org-element-map - (org-element-parse-buffer) - 'headline - (lambda (h) - (when (or (member tree-id (org-element-property :tags h)) - (equal tree-id (org-element-property :ID h))) h)))) - - -(defun rmh-elfeed-org-convert-tree-to-headlines (parsed-org) - "Get the inherited tags from PARSED-ORG structure if MATCH-FUNC is t. -The algorithm to gather inherited tags depends on the tree being -visited depth first by `org-element-map'. The reason I don't use -`org-get-tags-at' for this is that I can reuse the parsed org -structure and I am not dependent on the setting of -`org-use-tag-inheritance' or an org buffer being present at -all. Which in my opinion makes the process more traceable." - (let* ((tags '()) - (level 1)) - (org-element-map parsed-org 'headline - (lambda (h) - (pcase-let* - ((current-level (org-element-property :level h)) - (delta-level (- current-level level)) - (delta-tags (mapcar (lambda (tag) - (intern (substring-no-properties tag))) - (org-element-property :tags h))) - (heading (org-element-property :raw-value h)) - (`(,link ,description) - (org-element-map (org-element-property :title h) 'link - (lambda (link) - (list - (org-element-property :raw-link link) - (when (and (org-element-property :contents-begin link) - (org-element-property :contents-end link)) - (buffer-substring - (org-element-property :contents-begin link) - (org-element-property :contents-end link))))) - nil t))) - ;; update the tags stack when we visit a parent or sibling - (unless (> delta-level 0) - (let ((drop-num (+ 1 (- delta-level)))) - (setq tags (nthcdr drop-num tags)))) - ;; save current level to compare with next heading that will be visited - (setq level current-level) - ;; save the tags that might apply to potential children of the current heading - (push (append (car tags) delta-tags) tags) - ;; return the heading and inherited tags - (if (and link description) - (append (list link) - (car tags) - (list description)) - (append (list (if link link heading)) - (car tags)))))))) - -;; TODO: mark wrongly formatted feeds (PoC for unretrievable feeds) -(defun rmh-elfeed-org-flag-headlines (parsed-org) - "Flag headlines in PARSED-ORG if they don't have a valid value." - (org-element-map parsed-org 'headline - (lambda (h) - (let ((tags (org-element-property :tags h))) - (org-element-put-property h :tags (push "_flag_" tags)))))) - - -(defun rmh-elfeed-org-filter-relevant (list) - "Filter relevant entries from the LIST." - (cl-remove-if-not - (lambda (entry) - (and - (string-match-p "\\(http\\|gopher\\|file\\|entry-title\\)" (car entry)) - (not (member (intern rmh-elfeed-org-ignore-tag) entry)))) - list)) - - -(defun rmh-elfeed-org-cleanup-headlines (headlines tree-id) - "In all HEADLINES given remove the TREE-ID." - (mapcar (lambda (e) (delete tree-id e)) headlines)) - - -(defun rmh-elfeed-org-import-headlines-from-files (files tree-id) - "Visit all FILES and return the headlines stored under tree tagged TREE-ID or with the \":ID:\" TREE-ID in one list." - (cl-remove-duplicates - (mapcan (lambda (file) - (with-temp-buffer - (insert-file-contents (expand-file-name file org-directory)) - (let ((org-inhibit-startup t) - (org-mode-hook nil)) - (org-mode)) - (rmh-elfeed-org-cleanup-headlines - (rmh-elfeed-org-filter-relevant - (rmh-elfeed-org-convert-tree-to-headlines - (rmh-elfeed-org-import-trees tree-id))) - (intern tree-id)))) - files) - :test #'equal)) - - -(defun rmh-elfeed-org-convert-headline-to-tagger-params (tagger-headline) - "Add new entry hooks for tagging configured with the found headline in TAGGER-HEADLINE." - (list - (string-clean-whitespace - (string-remove-prefix "entry-title:" (car tagger-headline))) - (cdr tagger-headline))) - - -(defun rmh-elfeed-org-export-entry-hook (tagger-params) - "Export TAGGER-PARAMS to the proper `elfeed' structure." - (add-hook 'elfeed-org-new-entry-hook - (elfeed-make-tagger - :entry-title (nth 0 tagger-params) - :add (nth 1 tagger-params)))) - -(defun rmh-elfeed-org-export-feed (headline) - "Export HEADLINE to the proper `elfeed' structure." - (if (and (stringp (car (last headline))) - (> (length headline) 1)) - (progn - (add-to-list 'elfeed-feeds (butlast headline)) - (let ((feed (elfeed-db-get-feed (car headline))) - (title (substring-no-properties (car (last headline))))) - (setf (elfeed-meta feed :title) title) - (elfeed-meta feed :title))) - (add-to-list 'elfeed-feeds headline))) - -(defun rmh-elfeed-org-process (files tree-id) - "Process headlines and taggers from FILES with org headlines with TREE-ID." - - ;; Warn if configuration files are missing - (mapc #'rmh-elfeed-org-check-configuration-file files) - - ;; Clear elfeed structures - (setq elfeed-feeds nil) - (setq elfeed-org-new-entry-hook nil) - - ;; Convert org structure to elfeed structure and register taggers and subscriptions - (let* ((headlines (rmh-elfeed-org-import-headlines-from-files files tree-id)) - (subscriptions (rmh-elfeed-org-filter-subscriptions headlines)) - (taggers (rmh-elfeed-org-filter-taggers headlines)) - (elfeed-taggers (mapcar #'rmh-elfeed-org-convert-headline-to-tagger-params - (rmh-elfeed-org-filter-taggers headlines)))) - (mapc #'rmh-elfeed-org-export-feed subscriptions) - (mapc #'rmh-elfeed-org-export-entry-hook elfeed-taggers)) - - ;; Tell user what we did - (elfeed-log 'info "elfeed-org loaded %i feeds, %i rules" - (length elfeed-feeds) - (length elfeed-org-new-entry-hook))) - -(defun elfeed-org-run-new-entry-hook (entry) - "Run ENTRY through elfeed-org taggers." - (dolist (hook elfeed-org-new-entry-hook) - (funcall hook entry))) - -(defun rmh-elfeed-apply-autotags-now-advice () - "Make entry title matching rules works with `elfeed-apply-autotags-now'." - (interactive) - (let* ((headlines (rmh-elfeed-org-import-headlines-from-files - rmh-elfeed-org-files rmh-elfeed-org-tree-id)) - (taggers (rmh-elfeed-org-filter-taggers headlines)) - (elfeed-taggers (mapcar #'rmh-elfeed-org-convert-headline-to-tagger-params taggers)) - (entry-match-taggers (mapcar (lambda (tagger-params) - (elfeed-make-tagger - :entry-title (nth 0 tagger-params) - :add (nth 1 tagger-params))) - elfeed-taggers))) - (with-elfeed-db-visit (entry feed) - (dolist (tagger entry-match-taggers) - (funcall tagger entry))))) - -(defun rmh-elfeed-org-filter-taggers (headlines) - "Filter tagging rules from the HEADLINES in the tree." - (cl-remove-if-not #'identity - (mapcar - (lambda (headline) - (when (string-prefix-p "entry-title" (car headline)) - headline)) - headlines))) - -(defun rmh-elfeed-org-filter-subscriptions (headlines) - "Filter subscriptions to rss feeds from the HEADLINES in the tree." - (cl-remove-if-not #'identity - (mapcar - (lambda (headline) - (let* ((text (car headline)) - (link-and-title (and (string-match "^\\[\\[\\(http.+?\\)\\]\\[\\(.+?\\)\\]\\]" text) - (list (match-string-no-properties 0 text) - (match-string-no-properties 1 text) - (match-string-no-properties 2 text)))) - (hyperlink (and (string-match "^\\[\\[\\(http.+?\\)\\]\\(?:\\[.+?\\]\\)?\\]" text) - (list (match-string-no-properties 0 text) - (match-string-no-properties 1 text))))) - (cond ((string-prefix-p "http" text) headline) - (link-and-title (append (list (nth 1 hyperlink)) - (cdr headline) - (list (nth 2 link-and-title)))) - (hyperlink (append (list (nth 1 hyperlink)) (cdr headline)))))) - headlines))) - - -(defun rmh-elfeed-org-convert-opml-to-org (xml level) - "Convert OPML content to Org format. -Argument XML content of the OPML file. -Argument LEVEL current level in the tree." - (cl-loop for (tag attr . content) in (cl-remove-if-not #'listp xml) - when (and (not (assoc 'xmlUrl attr)) (assoc 'title attr)) - concat (format "%s %s\n" (make-string level ?*) (cdr it)) - when (assoc 'xmlUrl attr) - concat (format "%s [[%s][%s]]\n" (make-string level ?*) - (cdr it) (cdr (assoc 'title attr))) - concat (rmh-elfeed-org-convert-opml-to-org content (+ 1 level)))) - -(defun elfeed-org-import-opml (opml-file) - "Import feeds from OPML file to a temporary Org buffer. -Argument OPML-FILE filename of the OPML file." - (interactive "FInput OPML file: ") - (let* ((xml (xml-parse-file opml-file)) - (content (rmh-elfeed-org-convert-opml-to-org xml 0))) - (with-current-buffer (get-buffer-create "*Imported Org Feeds*") - (erase-buffer) - (insert (format "* Imported Feeds :%s:\n" rmh-elfeed-org-tree-id)) - (insert content) - (let ((org-inhibit-startup t)) - (org-mode)) - (pop-to-buffer (current-buffer))))) - - -(defun rmh-elfeed-org-convert-org-to-opml (org-buffer) - "Convert Org buffer content to OPML format. -Argument ORG-BUFFER the buffer to write the OPML content to." - (let (need-ends - opml-body) - (with-current-buffer org-buffer - (let ((org-inhibit-startup t) - (org-mode-hook nil)) - (org-mode)) - (org-element-map (rmh-elfeed-org-import-trees - rmh-elfeed-org-tree-id) 'headline - (lambda (h) - (let* ((current-level (org-element-property :level h)) - (tags (org-element-property :tags h)) - (heading (org-element-property :raw-value h)) - (link-and-title (and (string-match "^\\[\\[\\(http.+?\\)\\]\\[\\(.+?\\)\\]\\]" heading) - (list (match-string-no-properties 0 heading) - (match-string-no-properties 1 heading) - (match-string-no-properties 2 heading)))) - (hyperlink (and (string-match "^\\[\\[\\(http.+?\\)\\]\\(?:\\[.+?\\]\\)?\\]" heading) - (list (match-string-no-properties 0 heading) - (match-string-no-properties 1 heading)))) - url - title - opml-outline) - ;; fill missing end outlines - (while (and (car need-ends) (>= (car need-ends) current-level)) - (let* ((level (pop need-ends))) - (setq opml-body (concat opml-body (format " %s\n" - (make-string (* 2 level) ? )))))) - - (cond ((string-prefix-p "http" heading) - (setq url heading) - (setq title (or (elfeed-feed-title (elfeed-db-get-feed heading)) "Unknown"))) - (link-and-title (setq url (nth 1 link-and-title)) - (setq title (nth 2 link-and-title))) - (hyperlink (setq url (nth 1 hyperlink)) - (setq title (or (elfeed-feed-title (elfeed-db-get-feed (nth 1 hyperlink))) "Unknown"))) - (t (setq title heading))) - (if url - (setq opml-outline (format " %s\n" - (make-string (* 2 current-level) ? ) - (xml-escape-string title) - (xml-escape-string url))) - (unless (string-prefix-p "entry-title" heading) - (unless (member rmh-elfeed-org-tree-id tags) - ;; insert category title only when it is neither the top - ;; level elfeed node nor the entry-title node - (progn - (push current-level need-ends) - (setq opml-outline (format " %s\n" - (make-string (* 2 current-level) ? ) - (xml-escape-string title))))))) - (setq opml-body (concat opml-body opml-outline)))))) - - ;; fill missing end outlines at end - (while (car need-ends) - (let* ((level (pop need-ends))) - (setq opml-body (concat opml-body (format " %s\n" - (make-string (* 2 level) ? )))))) - opml-body)) - -(defun elfeed-org-export-opml () - "Export Org feeds under `rmh-elfeed-org-files' to a temporary OPML buffer. -The first level elfeed node will be ignored. The user may need edit the output -because most of Feed/RSS readers only support trees of 2 levels deep." - (interactive) - (let ((opml-body (cl-loop for org-file in rmh-elfeed-org-files - concat - (with-temp-buffer - (insert-file-contents - (expand-file-name org-file org-directory)) - (rmh-elfeed-org-convert-org-to-opml - (current-buffer)))))) - (with-current-buffer (get-buffer-create "*Exported OPML Feeds*") - (erase-buffer) - (insert "\n") - (insert "\n") - (insert " \n") - (insert " Elfeed-Org Export\n") - (insert " \n") - (insert " \n") - (insert opml-body) - (insert " \n") - (insert "\n") - (xml-mode) - (pop-to-buffer (current-buffer))))) - -(defun rmh-elfeed-org-process-advice () - "Advice to add to `elfeed' to load the configuration before it is run." - (rmh-elfeed-org-process rmh-elfeed-org-files rmh-elfeed-org-tree-id)) - -;;;###autoload -(defun elfeed-org () - "Hook up rmh-elfeed-org to read the `org-mode' configuration when elfeed is run." - (interactive) - (elfeed-log 'info "elfeed-org is set up to handle elfeed configuration") - (advice-add #'elfeed :before #'rmh-elfeed-org-process-advice) - (add-hook 'elfeed-new-entry-hook #'elfeed-org-run-new-entry-hook) - (advice-add 'elfeed-apply-autotags-now :after #'rmh-elfeed-apply-autotags-now-advice) - (add-hook 'elfeed-http-error-hooks - (lambda (url _status) - (when rmh-elfeed-org-auto-ignore-invalid-feeds - (rmh-elfeed-org-mark-feed-ignore url)))) - (add-hook 'elfeed-parse-error-hooks - (lambda (url _error) - (when rmh-elfeed-org-auto-ignore-invalid-feeds - (rmh-elfeed-org-mark-feed-ignore url))))) - - -(provide 'elfeed-org) -;;; elfeed-org.el ends here diff --git a/elpa/elfeed-org-20250219.950/elfeed-org-autoloads.el b/elpa/elfeed-org-20250219.950/elfeed-org-autoloads.el new file mode 100644 index 00000000..ac9aaa0b --- /dev/null +++ b/elpa/elfeed-org-20250219.950/elfeed-org-autoloads.el @@ -0,0 +1,30 @@ +;;; elfeed-org-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + +(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) + + + +;;; Generated autoloads from elfeed-org.el + +(autoload 'elfeed-org "elfeed-org" "\ +Hook up rmh-elfeed-org to read the `org-mode' configuration when elfeed is run." t) +(register-definition-prefixes "elfeed-org" '("elfeed-org-" "rmh-elfeed-")) + +;;; End of scraped data + +(provide 'elfeed-org-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; elfeed-org-autoloads.el ends here diff --git a/elpa/elfeed-org-20250219.950/elfeed-org-pkg.el b/elpa/elfeed-org-20250219.950/elfeed-org-pkg.el new file mode 100644 index 00000000..d624759c --- /dev/null +++ b/elpa/elfeed-org-20250219.950/elfeed-org-pkg.el @@ -0,0 +1,12 @@ +;; -*- no-byte-compile: t; lexical-binding: nil -*- +(define-package "elfeed-org" "20250219.950" + "Configure elfeed with one or more org-mode files." + '((emacs "28.1") + (elfeed "1.1.1") + (org "8.2.7")) + :url "https://github.com/remyhonig/elfeed-org" + :commit "1197cf29f6604e572ec604874a8f50b58081176a" + :revdesc "1197cf29f660" + :keywords '("news") + :authors '(("Remy Honig" . "remyhonig@gmail.com")) + :maintainers '(("Remy Honig" . "remyhonig@gmail.com"))) diff --git a/elpa/elfeed-org-20250219.950/elfeed-org.el b/elpa/elfeed-org-20250219.950/elfeed-org.el new file mode 100644 index 00000000..fead0ed0 --- /dev/null +++ b/elpa/elfeed-org-20250219.950/elfeed-org.el @@ -0,0 +1,429 @@ +;;; elfeed-org.el --- Configure elfeed with one or more org-mode files -*- lexical-binding: t; -*- + +;; Copyright (C) 2014 Remy Honig + +;; Author : Remy Honig +;; Package-Requires : ((emacs "28.1") (elfeed "1.1.1") (org "8.2.7")) +;; URL : https://github.com/remyhonig/elfeed-org +;; Version : 20250104.0 +;; Keywords : news + +;; 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 . + +;; Package-Version: 20250219.950 +;; Package-Revision: 1197cf29f660 + +;;; Commentary: +;; Maintaining tags for all rss feeds is cumbersome using the regular +;; flat list where there is no hierarchy and tag names are duplicated +;; a lot. Org-mode makes the book keeping of tags and feeds much +;; easier. Tags get inherited from parent headlines. Multiple files +;; can be specified to separate your private from your work feeds for +;; example. You may also use tagging rules to tag feeds by entry-title +;; keywords. See https://github.com/remyhonig/elfeed-org for usage. + +;;; Code: + +(require 'elfeed) +(require 'org) +(require 'org-element) +(require 'cl-lib) +(require 'xml) + + +(defgroup elfeed-org nil + "Configure the Elfeed RSS reader with an Orgmode file" + :group 'comm) + + +(defcustom rmh-elfeed-org-tree-id "elfeed" + "The tag or ID property on the trees containing the RSS feeds." + :group 'elfeed-org + :type 'string) + +(defcustom rmh-elfeed-org-ignore-tag "ignore" + "The tag on the feed trees that will be ignored." + :group 'elfeed-org + :type 'string) + +(defcustom rmh-elfeed-org-auto-ignore-invalid-feeds nil + "Tag feeds to ignore them when a feed could not loaded." + :group 'elfeed-org + :type 'bool) + +(defcustom rmh-elfeed-org-files (list (locate-user-emacs-file "elfeed.org")) + "The files where we look to find trees with the `rmh-elfeed-org-tree-id'. +In this file paths can be given relative to `org-directory'." + :group 'elfeed-org + :type '(repeat (file :tag "org-mode file"))) + +(defvar elfeed-org-new-entry-hook nil + "List of new-entry tagger hooks created by elfeed-org.") + +(defun rmh-elfeed-org-check-configuration-file (file) + "Make sure FILE exists." + (when (not (file-exists-p (expand-file-name file org-directory))) + (error "Elfeed-org cannot open %s. Make sure it exists or customize the variable \'rmh-elfeed-org-files\'" + (abbreviate-file-name file)))) + +(defun rmh-elfeed-org-is-headline-contained-in-elfeed-tree () + "Is any ancestor a headline with the elfeed tree id. +Return t if it does or nil if it does not." + (let ((result nil)) + (save-excursion + (while (and (not result) (org-up-heading-safe)) + (setq result (member rmh-elfeed-org-tree-id (org-get-tags)))) + result))) + +(defun rmh-elfeed-org-mark-feed-ignore (url) + "Set tag `rmh-elfeed-org-ignore-tag' to headlines containing the feed URL." + (let ((org-inhibit-startup t)) + (dolist (org-file rmh-elfeed-org-files) + (with-current-buffer (find-file-noselect + (expand-file-name org-file)) + (org-mode) + (goto-char (point-min)) + (while (and + (search-forward url nil t) + ;; Prefer outline-on-heading-p because org-on-heading-p + ;; is obsolete but org-at-heading-p was only introduced + ;; in org 9.0: + (outline-on-heading-p t) + (rmh-elfeed-org-is-headline-contained-in-elfeed-tree)) + (org-toggle-tag rmh-elfeed-org-ignore-tag 'on)) + (elfeed-log 'info "elfeed-org tagged '%s' in file '%s' with '%s' to be ignored" url org-file rmh-elfeed-org-ignore-tag))))) + +(defun rmh-elfeed-org-import-trees (tree-id) + "Get trees with \":ID:\" property or tag of value TREE-ID. +Return trees with TREE-ID as the value of the id property or +with a tag of the same value. Setting an \":ID:\" property is not +recommended but I support it for backward compatibility of +current users." + (org-element-map + (org-element-parse-buffer) + 'headline + (lambda (h) + (when (or (member tree-id (org-element-property :tags h)) + (equal tree-id (org-element-property :ID h))) h)))) + + +(defun rmh-elfeed-org-convert-tree-to-headlines (parsed-org) + "Get the inherited tags from PARSED-ORG structure if MATCH-FUNC is t. +The algorithm to gather inherited tags depends on the tree being +visited depth first by `org-element-map'. The reason I don't use +`org-get-tags-at' for this is that I can reuse the parsed org +structure and I am not dependent on the setting of +`org-use-tag-inheritance' or an org buffer being present at +all. Which in my opinion makes the process more traceable." + (let* ((tags '()) + (level 1)) + (org-element-map parsed-org 'headline + (lambda (h) + (pcase-let* + ((current-level (org-element-property :level h)) + (delta-level (- current-level level)) + (delta-tags (mapcar (lambda (tag) + (intern (substring-no-properties tag))) + (org-element-property :tags h))) + (heading (org-element-property :raw-value h)) + (`(,link ,description) + (org-element-map (org-element-property :title h) 'link + (lambda (link) + (list + (org-element-property :raw-link link) + (when (and (org-element-property :contents-begin link) + (org-element-property :contents-end link)) + (buffer-substring + (org-element-property :contents-begin link) + (org-element-property :contents-end link))))) + nil t))) + ;; update the tags stack when we visit a parent or sibling + (unless (> delta-level 0) + (let ((drop-num (+ 1 (- delta-level)))) + (setq tags (nthcdr drop-num tags)))) + ;; save current level to compare with next heading that will be visited + (setq level current-level) + ;; save the tags that might apply to potential children of the current heading + (push (append (car tags) delta-tags) tags) + ;; return the heading and inherited tags + (if (and link description) + (append (list link) + (car tags) + (list description)) + (append (list (if link link heading)) + (car tags)))))))) + +;; TODO: mark wrongly formatted feeds (PoC for unretrievable feeds) +(defun rmh-elfeed-org-flag-headlines (parsed-org) + "Flag headlines in PARSED-ORG if they don't have a valid value." + (org-element-map parsed-org 'headline + (lambda (h) + (let ((tags (org-element-property :tags h))) + (org-element-put-property h :tags (push "_flag_" tags)))))) + + +(defun rmh-elfeed-org-filter-relevant (list) + "Filter relevant entries from the LIST." + (cl-remove-if-not + (lambda (entry) + (and + (string-match-p "\\(http\\|gopher\\|file\\|entry-title\\)" (car entry)) + (not (member (intern rmh-elfeed-org-ignore-tag) entry)))) + list)) + + +(defun rmh-elfeed-org-cleanup-headlines (headlines tree-id) + "In all HEADLINES given remove the TREE-ID." + (mapcar (lambda (e) (delete tree-id e)) headlines)) + + +(defun rmh-elfeed-org-import-headlines-from-files (files tree-id) + "Visit all FILES and return the headlines stored under tree tagged TREE-ID or with the \":ID:\" TREE-ID in one list." + (cl-remove-duplicates + (mapcan (lambda (file) + (with-temp-buffer + (insert-file-contents (expand-file-name file org-directory)) + (let ((org-inhibit-startup t) + (org-mode-hook nil)) + (org-mode)) + (rmh-elfeed-org-cleanup-headlines + (rmh-elfeed-org-filter-relevant + (rmh-elfeed-org-convert-tree-to-headlines + (rmh-elfeed-org-import-trees tree-id))) + (intern tree-id)))) + files) + :test #'equal)) + + +(defun rmh-elfeed-org-convert-headline-to-tagger-params (tagger-headline) + "Add new entry hooks for tagging configured with the found headline in TAGGER-HEADLINE." + (list + (string-clean-whitespace + (string-remove-prefix "entry-title:" (car tagger-headline))) + (cdr tagger-headline))) + + +(defun rmh-elfeed-org-export-entry-hook (tagger-params) + "Export TAGGER-PARAMS to the proper `elfeed' structure." + (add-hook 'elfeed-org-new-entry-hook + (elfeed-make-tagger + :entry-title (nth 0 tagger-params) + :add (nth 1 tagger-params)))) + +(defun rmh-elfeed-org-export-feed (headline) + "Export HEADLINE to the proper `elfeed' structure." + (if (and (stringp (car (last headline))) + (> (length headline) 1)) + (progn + (add-to-list 'elfeed-feeds (butlast headline)) + (let ((feed (elfeed-db-get-feed (car headline))) + (title (substring-no-properties (car (last headline))))) + (setf (elfeed-meta feed :title) title) + (elfeed-meta feed :title))) + (add-to-list 'elfeed-feeds headline))) + +(defun rmh-elfeed-org-process (files tree-id) + "Process headlines and taggers from FILES with org headlines with TREE-ID." + + ;; Warn if configuration files are missing + (mapc #'rmh-elfeed-org-check-configuration-file files) + + ;; Clear elfeed structures + (setq elfeed-feeds nil) + (setq elfeed-org-new-entry-hook nil) + + ;; Convert org structure to elfeed structure and register taggers and subscriptions + (let* ((headlines (rmh-elfeed-org-import-headlines-from-files files tree-id)) + (subscriptions (rmh-elfeed-org-filter-subscriptions headlines)) + (taggers (rmh-elfeed-org-filter-taggers headlines)) + (elfeed-taggers (mapcar #'rmh-elfeed-org-convert-headline-to-tagger-params taggers))) + (mapc #'rmh-elfeed-org-export-feed subscriptions) + (mapc #'rmh-elfeed-org-export-entry-hook elfeed-taggers)) + + ;; Tell user what we did + (elfeed-log 'info "elfeed-org loaded %i feeds, %i rules" + (length elfeed-feeds) + (length elfeed-org-new-entry-hook))) + +(defun elfeed-org-run-new-entry-hook (entry) + "Run ENTRY through elfeed-org taggers." + (dolist (hook elfeed-org-new-entry-hook) + (funcall hook entry))) + +(defun rmh-elfeed-apply-autotags-now-advice () + "Make entry title matching rules works with `elfeed-apply-autotags-now'." + (interactive) + (let* ((headlines (rmh-elfeed-org-import-headlines-from-files + rmh-elfeed-org-files rmh-elfeed-org-tree-id)) + (taggers (rmh-elfeed-org-filter-taggers headlines)) + (elfeed-taggers (mapcar #'rmh-elfeed-org-convert-headline-to-tagger-params taggers)) + (entry-match-taggers (mapcar (lambda (tagger-params) + (elfeed-make-tagger + :entry-title (nth 0 tagger-params) + :add (nth 1 tagger-params))) + elfeed-taggers))) + (with-elfeed-db-visit (entry feed) + (dolist (tagger entry-match-taggers) + (funcall tagger entry))))) + +(defun rmh-elfeed-org-filter-taggers (headlines) + "Filter tagging rules from the HEADLINES in the tree." + (cl-remove-if-not (lambda (headline) (string-prefix-p "entry-title" (car headline))) + headlines)) + +(defun rmh-elfeed-org-filter-subscriptions (headlines) + "Filter subscriptions to rss feeds from the HEADLINES in the tree." + (cl-remove-if (lambda (headline) (string-prefix-p "entry-title" (car headline))) + headlines)) + +(defun rmh-elfeed-org-convert-opml-to-org (xml level) + "Convert OPML content to Org format. +Argument XML content of the OPML file. +Argument LEVEL current level in the tree." + (cl-loop for (tag attr . content) in (cl-remove-if-not #'listp xml) + when (and (not (assoc 'xmlUrl attr)) (assoc 'title attr)) + concat (format "%s %s\n" (make-string level ?*) (cdr it)) + when (assoc 'xmlUrl attr) + concat (format "%s [[%s][%s]]\n" (make-string level ?*) + (cdr it) (cdr (assoc 'title attr))) + concat (rmh-elfeed-org-convert-opml-to-org content (+ 1 level)))) + +(defun elfeed-org-import-opml (opml-file) + "Import feeds from OPML file to a temporary Org buffer. +Argument OPML-FILE filename of the OPML file." + (interactive "FInput OPML file: ") + (let* ((xml (xml-parse-file opml-file)) + (content (rmh-elfeed-org-convert-opml-to-org xml 0))) + (with-current-buffer (get-buffer-create "*Imported Org Feeds*") + (erase-buffer) + (insert (format "* Imported Feeds :%s:\n" rmh-elfeed-org-tree-id)) + (insert content) + (let ((org-inhibit-startup t)) + (org-mode)) + (pop-to-buffer (current-buffer))))) + + +(defun rmh-elfeed-org-convert-org-to-opml (org-buffer) + "Convert Org buffer content to OPML format. +Argument ORG-BUFFER the buffer to write the OPML content to." + (let (need-ends + opml-body) + (with-current-buffer org-buffer + (let ((org-inhibit-startup t) + (org-mode-hook nil)) + (org-mode)) + (org-element-map (rmh-elfeed-org-import-trees + rmh-elfeed-org-tree-id) + 'headline + (lambda (h) + (let* ((current-level (org-element-property :level h)) + (tags (org-element-property :tags h)) + (heading (org-element-property :raw-value h)) + (link-and-title (and (string-match "^\\[\\[\\(http.+?\\)\\]\\[\\(.+?\\)\\]\\]" heading) + (list (match-string-no-properties 0 heading) + (match-string-no-properties 1 heading) + (match-string-no-properties 2 heading)))) + (hyperlink (and (string-match "^\\[\\[\\(http.+?\\)\\]\\(?:\\[.+?\\]\\)?\\]" heading) + (list (match-string-no-properties 0 heading) + (match-string-no-properties 1 heading)))) + url + title + opml-outline) + ;; fill missing end outlines + (while (and (car need-ends) (>= (car need-ends) current-level)) + (let* ((level (pop need-ends))) + (setq opml-body (concat opml-body (format " %s\n" + (make-string (* 2 level) ? )))))) + + (cond ((string-prefix-p "http" heading) + (setq url heading) + (setq title (or (elfeed-feed-title (elfeed-db-get-feed heading)) "Unknown"))) + (link-and-title (setq url (nth 1 link-and-title)) + (setq title (nth 2 link-and-title))) + (hyperlink (setq url (nth 1 hyperlink)) + (setq title (or (elfeed-feed-title (elfeed-db-get-feed (nth 1 hyperlink))) "Unknown"))) + (t (setq title heading))) + (if url + (setq opml-outline (format " %s\n" + (make-string (* 2 current-level) ? ) + (xml-escape-string title) + (xml-escape-string url))) + (unless (string-prefix-p "entry-title" heading) + (unless (member rmh-elfeed-org-tree-id tags) + ;; insert category title only when it is neither the top + ;; level elfeed node nor the entry-title node + (progn + (push current-level need-ends) + (setq opml-outline (format " %s\n" + (make-string (* 2 current-level) ? ) + (xml-escape-string title))))))) + (setq opml-body (concat opml-body opml-outline)))))) + + ;; fill missing end outlines at end + (while (car need-ends) + (let* ((level (pop need-ends))) + (setq opml-body (concat opml-body (format " %s\n" + (make-string (* 2 level) ? )))))) + opml-body)) + +(defun elfeed-org-export-opml () + "Export Org feeds under `rmh-elfeed-org-files' to a temporary OPML buffer. +The first level elfeed node will be ignored. The user may need edit the output +because most of Feed/RSS readers only support trees of 2 levels deep." + (interactive) + (let ((opml-body (cl-loop for org-file in rmh-elfeed-org-files + concat + (with-temp-buffer + (insert-file-contents + (expand-file-name org-file org-directory)) + (rmh-elfeed-org-convert-org-to-opml + (current-buffer)))))) + (with-current-buffer (get-buffer-create "*Exported OPML Feeds*") + (erase-buffer) + (insert "\n") + (insert "\n") + (insert " \n") + (insert " Elfeed-Org Export\n") + (insert " \n") + (insert " \n") + (insert opml-body) + (insert " \n") + (insert "\n") + (xml-mode) + (pop-to-buffer (current-buffer))))) + +(defun rmh-elfeed-org-process-advice () + "Advice to add to `elfeed' to load the configuration before it is run." + (rmh-elfeed-org-process rmh-elfeed-org-files rmh-elfeed-org-tree-id)) + +;;;###autoload +(defun elfeed-org () + "Hook up rmh-elfeed-org to read the `org-mode' configuration when elfeed is run." + (interactive) + (elfeed-log 'info "elfeed-org is set up to handle elfeed configuration") + (advice-add #'elfeed :before #'rmh-elfeed-org-process-advice) + (add-hook 'elfeed-new-entry-hook #'elfeed-org-run-new-entry-hook) + (advice-add 'elfeed-apply-autotags-now :after #'rmh-elfeed-apply-autotags-now-advice) + (add-hook 'elfeed-http-error-hooks + (lambda (url _status) + (when rmh-elfeed-org-auto-ignore-invalid-feeds + (rmh-elfeed-org-mark-feed-ignore url)))) + (add-hook 'elfeed-parse-error-hooks + (lambda (url _error) + (when rmh-elfeed-org-auto-ignore-invalid-feeds + (rmh-elfeed-org-mark-feed-ignore url))))) + + +(provide 'elfeed-org) +;;; elfeed-org.el ends here