Commit 70abefc0a6

70abefc0a63967ff5b2d57050538b16d701696c6

parent: fcc889508a

Unsigned

cmc <hello@cleberg.net> · 2023-12-03 05:26 UTC

feat: add initial RSS support and centralize project settings

Layout: unified · split

elisp/ox-rss.el added +434
@@ -0,0 +1,434 @@
1;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
2
3;; Copyright (C) 2013-2021 Free Software Foundation, Inc.
4
5;; Author: Bastien Guerry <bzg@gnu.org>
6;; Maintainer: Benedict Wang <foss@bhw.name>
7;; Version: 0.0.1
8;; Package-Requires: ((emacs "26.1") (org "9.3"))
9;; Keywords: org, wp, blog, feed, rss
10;; Homepage: https://github.com/benedicthw/ox-rss.git
11
12;; This file is not part of GNU Emacs.
13
14;; This program is free software: you can redistribute it and/or modify
15;; it under the terms of the GNU General Public License as published by
16;; the Free Software Foundation, either version 3 of the License, or
17;; (at your option) any later version.
18
19;; This program is distributed in the hope that it will be useful,
20;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22;; GNU General Public License for more details.
23
24;; You should have received a copy of the GNU General Public License
25;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
26
27;;; Commentary:
28
29;; This library implements an RSS 2.0 back-end for Org exporter, based
30;; on the `html' back-end.
31;;
32;; It requires Emacs 24.1 at least.
33;;
34;; It provides two commands for export, depending on the desired output:
35;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss'
36;; (as a ".xml" file).
37;;
38;; This backend understands three new option keywords:
39;;
40;; #+RSS_EXTENSION: xml
41;; #+RSS_IMAGE_URL: https://myblog.org/mypicture.jpg
42;; #+RSS_FEED_URL: https://myblog.org/feeds/blog.xml
43;;
44;; It uses #+HTML_LINK_HOME: to set the base url of the feed.
45;;
46;; Exporting an Org file to RSS modifies each top-level entry by adding a
47;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add
48;; an ID property, later used as the guid for the feed's item.
49;;
50;; The top-level headline is used as the title of each RSS item unless
51;; an RSS_TITLE property is set on the headline.
52;;
53;; You typically want to use it within a publishing project like this:
54;;
55;; (add-to-list
56;; 'org-publish-project-alist
57;; '("homepage_rss"
58;; :base-directory "~/myhomepage/"
59;; :base-extension "org"
60;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png"
61;; :html-link-home "http://lumiere.ens.fr/~guerry/"
62;; :html-link-use-abs-url t
63;; :rss-extension "xml"
64;; :publishing-directory "/home/guerry/public_html/"
65;; :publishing-function (org-rss-publish-to-rss)
66;; :section-numbers nil
67;; :exclude ".*" ;; To exclude all files...
68;; :include ("index.org") ;; ... except index.org.
69;; :table-of-contents nil))
70;;
71;; ... then rsync /home/guerry/public_html/ with your server.
72;;
73;; By default, the permalink for a blog entry points to the headline.
74;; You can specify a different one by using the :RSS_PERMALINK:
75;; property within an entry.
76
77;;; Code:
78
79(require 'ox-html)
80(declare-function url-encode-url "url-util" (url))
81
82;;; Variables and options
83
84(defgroup org-export-rss nil
85 "Options specific to RSS export back-end."
86 :tag "Org RSS"
87 :group 'org-export
88 :version "24.4"
89 :package-version '(Org . "8.0"))
90
91(defcustom org-rss-image-url "https://orgmode.org/img/org-mode-unicorn-logo.png"
92 "The URL of the image for the RSS feed."
93 :group 'org-export-rss
94 :type 'string)
95
96(defcustom org-rss-extension "xml"
97 "File extension for the RSS 2.0 feed."
98 :group 'org-export-rss
99 :type 'string)
100
101(defcustom org-rss-categories 'from-tags
102 "Where to extract items category information from.
103The default is to extract categories from the tags of the
104headlines. When set to another value, extract the category
105from the :CATEGORY: property of the entry."
106 :group 'org-export-rss
107 :type '(choice
108 (const :tag "From tags" from-tags)
109 (const :tag "From the category property" from-category)))
110
111(defcustom org-rss-use-entry-url-as-guid t
112 "Use the URL for the <guid> metatag?
113When nil, Org will create ids using `org-icalendar-create-uid'."
114 :group 'org-export-rss
115 :type 'boolean)
116
117;;; Define backend
118
119(org-export-define-derived-backend 'rss 'html
120 :menu-entry
121 '(?r "Export to RSS"
122 ((?R "As RSS buffer"
123 (lambda (a s v b) (org-rss-export-as-rss a s v)))
124 (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
125 (?o "As RSS file and open"
126 (lambda (a s v b)
127 (if a (org-rss-export-to-rss t s v)
128 (org-open-file (org-rss-export-to-rss nil s v)))))))
129 :options-alist
130 '((:description "DESCRIPTION" nil nil newline)
131 (:keywords "KEYWORDS" nil nil space)
132 (:with-toc nil nil nil) ;; Never include HTML's toc
133 (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
134 (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
135 (:rss-feed-url "RSS_FEED_URL" nil nil t)
136 (:rss-categories nil nil org-rss-categories))
137 :filters-alist '((:filter-final-output . org-rss-final-function))
138 :translate-alist '((headline . org-rss-headline)
139 (comment . (lambda (&rest args) ""))
140 (comment-block . (lambda (&rest args) ""))
141 (timestamp . (lambda (&rest args) ""))
142 (plain-text . org-rss-plain-text)
143 (section . org-rss-section)
144 (template . org-rss-template)))
145
146;;; Export functions
147
148;;;###autoload
149(defun org-rss-export-as-rss (&optional async subtreep visible-only)
150 "Export current buffer to an RSS buffer.
151
152If narrowing is active in the current buffer, only export its
153narrowed part.
154
155If a region is active, export that region.
156
157A non-nil optional argument ASYNC means the process should happen
158asynchronously. The resulting buffer should be accessible
159through the `org-export-stack' interface.
160
161When optional argument SUBTREEP is non-nil, export the sub-tree
162at point, extracting information from the headline properties
163first.
164
165When optional argument VISIBLE-ONLY is non-nil, don't export
166contents of hidden elements.
167
168Export is done in a buffer named \"*Org RSS Export*\", which will
169be displayed when `org-export-show-temporary-export-buffer' is
170non-nil."
171 (interactive)
172 (let ((file (buffer-file-name (buffer-base-buffer))))
173 (org-icalendar-create-uid file 'warn-user)
174 (org-rss-add-pubdate-property))
175 (org-export-to-buffer 'rss "*Org RSS Export*"
176 async subtreep visible-only nil nil (lambda () (text-mode))))
177
178;;;###autoload
179(defun org-rss-export-to-rss (&optional async subtreep visible-only)
180 "Export current buffer to an RSS file.
181
182If narrowing is active in the current buffer, only export its
183narrowed part.
184
185If a region is active, export that region.
186
187A non-nil optional argument ASYNC means the process should happen
188asynchronously. The resulting file should be accessible through
189the `org-export-stack' interface.
190
191When optional argument SUBTREEP is non-nil, export the sub-tree
192at point, extracting information from the headline properties
193first.
194
195When optional argument VISIBLE-ONLY is non-nil, don't export
196contents of hidden elements.
197
198Return output file's name."
199 (interactive)
200 (let ((file (buffer-file-name (buffer-base-buffer))))
201 (org-icalendar-create-uid file 'warn-user)
202 (org-rss-add-pubdate-property))
203 (let ((outfile (org-export-output-file-name
204 (concat "." org-rss-extension) subtreep)))
205 (org-export-to-file 'rss outfile async subtreep visible-only)))
206
207;;;###autoload
208(defun org-rss-publish-to-rss (plist filename pub-dir)
209 "Publish an org file to RSS.
210
211FILENAME is the filename of the Org file to be published. PLIST
212is the property list for the given project. PUB-DIR is the
213publishing directory.
214
215Return output file name."
216 (let ((bf (get-file-buffer filename)))
217 (if bf
218 (with-current-buffer bf
219 (org-icalendar-create-uid filename 'warn-user)
220 (org-rss-add-pubdate-property)
221 (write-file filename))
222 (find-file filename)
223 (org-icalendar-create-uid filename 'warn-user)
224 (org-rss-add-pubdate-property)
225 (write-file filename) (kill-buffer)))
226 (org-publish-org-to
227 'rss filename (concat "." org-rss-extension) plist pub-dir))
228
229;;; Main transcoding functions
230
231(defun org-rss-headline (headline contents info)
232 "Transcode HEADLINE element into RSS format.
233CONTENTS is the headline contents. INFO is a plist used as a
234communication channel."
235 (if (> (org-export-get-relative-level headline info) 1)
236 (org-export-data-with-backend headline 'html info)
237 (unless (org-element-property :footnote-section-p headline)
238 (let* ((email (org-export-data (plist-get info :email) info))
239 (author (and (plist-get info :with-author)
240 (let ((auth (plist-get info :author)))
241 (and auth (org-export-data auth info)))))
242 (htmlext (plist-get info :html-extension))
243 (hl-number (org-export-get-headline-number headline info))
244 (hl-home (file-name-as-directory (plist-get info :html-link-home)))
245 (hl-pdir (plist-get info :publishing-directory))
246 (hl-perm (org-element-property :RSS_PERMALINK headline))
247 (anchor
248 (or (org-element-property :CUSTOM_ID headline)
249 (org-export-get-reference headline info)))
250 (category (org-rss-plain-text
251 (or (org-element-property :CATEGORY headline) "") info))
252 (pubdate0 (org-element-property :PUBDATE headline))
253 (pubdate (let ((system-time-locale "C"))
254 (if (and pubdate0 (not (string-empty-p pubdate0)))
255 (format-time-string
256 "%a, %d %b %Y %T %z"
257 (org-time-string-to-time pubdate0)))))
258 (title (org-rss-plain-text
259 (or (org-element-property :RSS_TITLE headline)
260 (replace-regexp-in-string
261 org-link-bracket-re
262 (lambda (m) (or (match-string 3 m)
263 (match-string 1 m)))
264 (org-element-property :raw-value headline))) info))
265 (publink
266 (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm))
267 (concat
268 (or hl-home hl-pdir)
269 (file-name-nondirectory
270 (file-name-sans-extension
271 (plist-get info :input-file))) "." htmlext "#" anchor)))
272 (guid (if org-rss-use-entry-url-as-guid
273 publink
274 (org-rss-plain-text
275 (or (org-element-property :ID headline)
276 (org-element-property :CUSTOM_ID headline)
277 publink)
278 info))))
279 (if (not pubdate) "" ;; Skip entries with no PUBDATE prop
280 (format
281 (concat
282 "<item>\n"
283 "<title>%s</title>\n"
284 "<link>%s</link>\n"
285 "<author>%s (%s)</author>\n"
286 "<guid isPermaLink=\"false\">%s</guid>\n"
287 "<pubDate>%s</pubDate>\n"
288 (org-rss-build-categories headline info) "\n"
289 "<description><![CDATA[%s]]></description>\n"
290 "</item>\n")
291 title publink email author guid pubdate contents))))))
292
293(defun org-rss-build-categories (headline info)
294 "Build categories for the RSS item from INFO.
295Fallback to the HEADLINE :CATEGORY property."
296 (if (eq (plist-get info :rss-categories) 'from-tags)
297 (mapconcat
298 (lambda (c) (format "<category><![CDATA[%s]]></category>" c))
299 (org-element-property :tags headline)
300 "\n")
301 (let ((c (org-element-property :CATEGORY headline)))
302 (format "<category><![CDATA[%s]]></category>" c))))
303
304(defun org-rss-template (contents info)
305 "Return complete document string after RSS conversion.
306CONTENTS is the transcoded contents string. INFO is a plist used
307as a communication channel."
308 (let ((style (plist-get info :rss-stylesheet)))
309 (concat
310 (format "<?xml version=\"1.0\" encoding=\"%s\"?>"
311 (symbol-name org-html-coding-system))
312 (if style
313 (format "\n<?xml-stylesheet type=\"text/xsl\" href=\"%s\"?>"
314 style))
315 "\n<rss version=\"2.0\"
316 xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
317 xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
318 xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
319 xmlns:atom=\"http://www.w3.org/2005/Atom\"
320 xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"
321 xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\"
322 xmlns:georss=\"http://www.georss.org/georss\"
323 xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"
324 xmlns:media=\"http://search.yahoo.com/mrss/\">"
325 "<channel>"
326 (org-rss-build-channel-info info) "\n"
327 contents
328 "</channel>\n"
329 "</rss>")))
330
331(defun org-rss-build-channel-info (info)
332 "Given plist INFO build the RSS channel information."
333 (let* ((system-time-locale "C")
334 (title (org-export-data (plist-get info :title) info))
335 (email (org-export-data (plist-get info :email) info))
336 (author (and (plist-get info :with-author)
337 (let ((auth (plist-get info :author)))
338 (and auth (org-export-data auth info)))))
339 (date (format-time-string "%a, %d %b %Y %T %z")) ;; RFC 882
340 (description (org-export-data (plist-get info :description) info))
341 (lang (plist-get info :language))
342 (keywords (plist-get info :keywords))
343 (rssext (plist-get info :rss-extension))
344 (blogurl (or (plist-get info :html-link-home)
345 (plist-get info :publishing-directory)))
346 (image (url-encode-url (plist-get info :rss-image-url)))
347 (ifile (plist-get info :input-file))
348 (publink
349 (or (plist-get info :rss-feed-url)
350 (concat (file-name-as-directory blogurl)
351 (file-name-nondirectory
352 (file-name-sans-extension ifile))
353 "." rssext))))
354 (format
355 "\n<title>%s</title>
356<atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
357<link>%s</link>
358<description><![CDATA[%s]]></description>
359<language>%s</language>
360<pubDate>%s</pubDate>
361<lastBuildDate>%s</lastBuildDate>
362<generator>%s</generator>
363<webMaster>%s (%s)</webMaster>
364<image>
365<url>%s</url>
366<title>%s</title>
367<link>%s</link>
368</image>
369"
370 title publink blogurl description lang date date
371 (concat (format "Emacs %d.%d"
372 emacs-major-version
373 emacs-minor-version)
374 " Org-mode " (org-version))
375 email author image title blogurl)))
376
377(defun org-rss-section (section contents info)
378 "Transcode SECTION element into RSS format.
379CONTENTS is the section contents. INFO is a plist used as
380a communication channel."
381 contents)
382
383(defun org-rss-timestamp (timestamp contents info)
384 "Transcode a TIMESTAMP object from Org to RSS.
385CONTENTS is nil. INFO is a plist holding contextual
386information."
387 (org-html-encode-plain-text
388 (org-timestamp-translate timestamp)))
389
390(defun org-rss-plain-text (contents info)
391 "Convert plain text CONTENTS into RSS encoded text.
392INFO is a plist used as a communication channel"
393 (let (output)
394 (setq output (org-html-encode-plain-text contents)
395 output (org-export-activate-smart-quotes
396 output :html info))))
397
398;;; Filters
399
400(defun org-rss-final-function (contents backend info)
401 "Prettify the RSS output.
402CONTENTS is the headline contents as a transcoded string. BACKEND
403is a symbol representation of the backend used. INFO is a plist
404used as a communication channel."
405 (with-temp-buffer
406 (xml-mode)
407 (insert contents)
408 (indent-region (point-min) (point-max))
409 (buffer-substring-no-properties (point-min) (point-max))))
410
411;;; Miscellaneous
412
413(defun org-rss-add-pubdate-property ()
414 "Set the PUBDATE property for top-level headlines."
415 (let (msg)
416 (org-map-entries
417 (lambda ()
418 (let* ((entry (org-element-at-point))
419 (level (org-element-property :level entry)))
420 (when (= level 1)
421 (unless (org-entry-get (point) "PUBDATE")
422 (setq msg t)
423 (org-set-property
424 "PUBDATE" (format-time-string
425 (cdr org-time-stamp-formats)))))))
426 nil nil 'comment 'archive)
427 (when msg
428 (message "Property PUBDATE added to top-level entries in %s"
429 (buffer-file-name))
430 (sit-for 2))))
431
432(provide 'ox-rss)
433
434;;; ox-rss.el ends here
elisp/publish.el added +58
@@ -0,0 +1,58 @@
1;; Publish org files to HTML
2
3;; Functions
4(defun org-rss-publish-to-rss (plist filename pub-dir)
5 "Publish RSS with PLIST, only when FILENAME is 'rss.org'.
6PUB-DIR is when the output will be placed."
7 (if (equal "rss.org" (file-name-nondirectory filename))
8 (org-rss-export-to-rss plist filename pub-dir)))
9
10;; Project publishing settings
11(setq org-publish-project-alist
12 '(("cleberg.net"
13 :base-directory "~/Source/cleberg.net/"
14 :publishing-function org-html-publish-to-html
15 :publishing-directory "~/Source/cleberg.net/public/"
16 :auto-sitemap t
17 :recursive t
18 :section-numbers nil
19 :with-author nil
20 :html-validation-link nil
21 :with-tags t
22 :html-head-include-default-style nil
23 :html-head-include-scripts nil
24 :html-head "<link rel=stylesheet href=/static/styles.css.min>"
25 :html-preamble "<nav><a href=/>Home</a><a href=/blog/>Blog</a><a href=/services/>Services</a><a href=/wiki/>Wiki</a></nav>")
26
27 ("rss"
28 :base-directory "~/Source/cleberg.net/blog/"
29 :base-extension "org"
30 :html-link-home "https://cleberg.net/"
31 :html-link-use-abs-url t
32 :rss-extension "xml"
33 :publishing-directory "~/Source/cleberg.net/public/blog/"
34 :publishing-function org-rss-publish-to-rss
35 :section-numbers nil
36 :exclude "index.org"
37 :author "cmc"
38 :email ""
39 ;; test
40 :html-link-org-files-as-html t
41 :auto-sitemap t
42 :sitemap-filename "rss.org"
43 :sitemap-title "cleberg.net"
44 :sitemap-style list
45 :sitemap-sort-files anti-chronologically
46 ;;:sitemap-function 'rw/format-rss-feed
47 ;;:sitemap-format-entry 'rw/format-rss-feed-entry
48 ;; end test
49 )
50
51 ("static"
52 :base-directory "~/Source/cleberg.net/static/"
53 :base-extension "css\\|el"
54 :publishing-directory "~/Source/cleberg.net/public/static/"
55 :publishing-function org-publish-attachment)
56 ("website" :components ("cleberg.net" "rss" "static"))))
57
58(provide 'publish)