From 9b53537b89d83fd7ade6a8707bb932d46564519a Mon Sep 17 00:00:00 2001 From: Arun Isaac Date: Sat, 18 Jul 2020 05:46:59 +0530 Subject: Rename ennu to ennum. * ennu.el: Rename to ... * ennum.el: ... this. Replace all instances of ennu with ennum. * ennu-html.el: Rename to ... * ennum-html.el: ... this. Replace all instances of ennu with ennum. * ennu-image.el: Rename to ... * ennum-image.el: ... this. Replace all instances of ennu with ennum. --- ennu-html.el | 383 ----------------------------------------- ennu-image.el | 55 ------ ennu.el | 524 ------------------------------------------------------- ennum-html.el | 383 +++++++++++++++++++++++++++++++++++++++++ ennum-image.el | 55 ++++++ ennum.el | 534 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 972 insertions(+), 962 deletions(-) delete mode 100644 ennu-html.el delete mode 100644 ennu-image.el delete mode 100644 ennu.el create mode 100644 ennum-html.el create mode 100644 ennum-image.el create mode 100644 ennum.el diff --git a/ennu-html.el b/ennu-html.el deleted file mode 100644 index 10025bc..0000000 --- a/ennu-html.el +++ /dev/null @@ -1,383 +0,0 @@ -;; -*- lexical-binding: t -*- - -(require 'ox) -(require 'subr-x) -(require 'xmlgen) - -(defconst ennu--iso-639-1-alist - '(("ab" . "аҧсуа бызшәа, аҧсшәа") - ("aa" . "Afaraf") - ("af" . "Afrikaans") - ("ak" . "Akan") - ("sq" . "Shqip") - ("am" . "አማርኛ") - ("ar" . "العربية") - ("an" . "aragonés") - ("hy" . "Հայերեն") - ("as" . "অসমীয়া") - ("av" . "авар мацӀ, магӀарул мацӀ") - ("ae" . "avesta") - ("ay" . "aymar aru") - ("az" . "azərbaycan dili") - ("bm" . "bamanankan") - ("ba" . "башҡорт теле") - ("eu" . "euskara, euskera") - ("be" . "беларуская мова") - ("bn" . "বাংলা") - ("bh" . "भोजपुरी") - ("bi" . "Bislama") - ("bs" . "bosanski jezik") - ("br" . "brezhoneg") - ("bg" . "български език") - ("my" . "ဗမာစာ") - ("ca" . "català") - ("ch" . "Chamoru") - ("ce" . "нохчийн мотт") - ("ny" . "chiCheŵa, chinyanja") - ("zh" . "中文 (Zhōngwén), 汉语, 漢語") - ("cv" . "чӑваш чӗлхи") - ("kw" . "Kernewek") - ("co" . "corsu, lingua corsa") - ("cr" . "ᓀᐦᐃᔭᐍᐏᐣ") - ("hr" . "hrvatski jezik") - ("cs" . "čeština, český jazyk") - ("da" . "dansk") - ("dv" . "ދިވެހި") - ("nl" . "Nederlands, Vlaams") - ("dz" . "རྫོང་ཁ") - ("en" . "English") - ("eo" . "Esperanto") - ("et" . "eesti, eesti keel") - ("ee" . "Eʋegbe") - ("fo" . "føroyskt") - ("fj" . "vosa Vakaviti") - ("fi" . "suomi, suomen kieli") - ("fr" . "français, langue française") - ("ff" . "Fulfulde, Pulaar, Pular") - ("gl" . "galego") - ("ka" . "ქართული") - ("de" . "Deutsch") - ("el" . "ελληνικά") - ("gn" . "Avañe'ẽ") - ("gu" . "ગુજરાતી") - ("ht" . "Kreyòl ayisyen") - ("ha" . "(Hausa) هَوُسَ") - ("he" . "עברית") - ("hz" . "Otjiherero") - ("hi" . "हिन्दी, हिंदी") - ("ho" . "Hiri Motu") - ("hu" . "magyar") - ("ia" . "Interlingua") - ("id" . "Bahasa Indonesia") - ("ie" . "Originally called Occidental; then Interlingue after WWII") - ("ga" . "Gaeilge") - ("ig" . "Asụsụ Igbo") - ("ik" . "Iñupiaq, Iñupiatun") - ("io" . "Ido") - ("is" . "Íslenska") - ("it" . "Italiano") - ("iu" . "ᐃᓄᒃᑎᑐᑦ") - ("ja" . "日本語 (にほんご)") - ("jv" . "ꦧꦱꦗꦮ, Basa Jawa") - ("kl" . "kalaallisut, kalaallit oqaasii") - ("kn" . "ಕನ್ನಡ") - ("kr" . "Kanuri") - ("ks" . "कश्मीरी, كشميري‎") - ("kk" . "қазақ тілі") - ("km" . "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ") - ("ki" . "Gĩkũyũ") - ("rw" . "Ikinyarwanda") - ("ky" . "Кыргызча, Кыргыз тили") - ("kv" . "коми кыв") - ("kg" . "Kikongo") - ("ko" . "한국어") - ("ku" . "Kurdî, كوردی‎") - ("kj" . "Kuanyama") - ("la" . "latine, lingua latina") - ("lb" . "Lëtzebuergesch") - ("lg" . "Luganda") - ("li" . "Limburgs") - ("ln" . "Lingála") - ("lo" . "ພາສາລາວ") - ("lt" . "lietuvių kalba") - ("lu" . "Tshiluba") - ("lv" . "latviešu valoda") - ("gv" . "Gaelg, Gailck") - ("mk" . "македонски јазик") - ("mg" . "fiteny malagasy") - ("ms" . "bahasa Melayu, بهاس ملايو‎") - ("ml" . "മലയാളം") - ("mt" . "Malti") - ("mi" . "te reo Māori") - ("mr" . "मराठी") - ("mh" . "Kajin M̧ajeļ") - ("mn" . "Монгол хэл") - ("na" . "Dorerin Naoero") - ("nv" . "Diné bizaad") - ("nd" . "isiNdebele") - ("ne" . "नेपाली") - ("ng" . "Owambo") - ("nb" . "Norsk bokmål") - ("nn" . "Norsk nynorsk") - ("no" . "Norsk") - ("ii" . "ꆈꌠ꒿ Nuosuhxop") - ("nr" . "isiNdebele") - ("oc" . "occitan, lenga d'òc") - ("oj" . "ᐊᓂᔑᓈᐯᒧᐎᓐ") - ("cu" . "ѩзыкъ словѣньскъ") - ("om" . "Afaan Oromoo") - ("or" . "ଓଡ଼ିଆ") - ("os" . "ирон æвзаг") - ("pa" . "ਪੰਜਾਬੀ") - ("pi" . "पाऴि") - ("fa" . "فارسی") - ("pl" . "język polski, polszczyzna") - ("ps" . "پښتو") - ("pt" . "Português") - ("qu" . "Runa Simi, Kichwa") - ("rm" . "rumantsch grischun") - ("rn" . "Ikirundi") - ("ro" . "Română") - ("ru" . "Русский") - ("sa" . "संस्कृतम्") - ("sc" . "sardu") - ("sd" . "सिन्धी, سنڌي، سندھی‎") - ("se" . "Davvisámegiella") - ("sm" . "gagana fa'a Samoa") - ("sg" . "yângâ tî sängö") - ("sr" . "српски језик") - ("gd" . "Gàidhlig") - ("sn" . "chiShona") - ("si" . "සිංහල") - ("sk" . "slovenčina, slovenský jazyk") - ("sl" . "slovenski jezik, slovenščina") - ("so" . "Soomaaliga, af Soomaali") - ("st" . "Sesotho") - ("es" . "Español") - ("su" . "Basa Sunda") - ("sw" . "Kiswahili") - ("ss" . "SiSwati") - ("sv" . "svenska") - ("ta" . "தமிழ்") - ("te" . "తెలుగు") - ("tg" . "тоҷикӣ, toçikī, تاجیکی‎") - ("th" . "ไทย") - ("ti" . "ትግርኛ") - ("bo" . "བོད་ཡིག") - ("tk" . "Türkmen, Түркмен") - ("tl" . "Wikang Tagalog") - ("tn" . "Setswana") - ("to" . "faka Tonga") - ("tr" . "Türkçe") - ("ts" . "Xitsonga") - ("tt" . "татар теле, tatar tele") - ("tw" . "Twi") - ("ty" . "Reo Tahiti") - ("ug" . "ئۇيغۇرچە‎, Uyghurche") - ("uk" . "Українська") - ("ur" . "اردو") - ("uz" . "Oʻzbek, Ўзбек, أۇزبېك‎") - ("ve" . "Tshivenḓa") - ("vi" . "Tiếng Việt") - ("vo" . "Volapük") - ("wa" . "walon") - ("cy" . "Cymraeg") - ("wo" . "Wollof") - ("fy" . "Frysk") - ("xh" . "isiXhosa") - ("yi" . "ייִדיש") - ("yo" . "Yorùbá") - ("za" . "Saɯ cueŋƅ, Saw cuengh") - ("zu" . "isiZulu"))) - -(defun expand-file-name* (name default-directory) - (expand-file-name name (concat "/" default-directory))) - -(org-export-define-derived-backend 'ennu-html 'html - :translate-alist - '((inner-template . ennu-html-inner-template) - (link . ennu-html-link)) - :options-alist - '((:summary "SUMMARY" nil nil parse) - (:thumbnail "THUMBNAIL" nil nil t) - (:translation-group "TRANSLATION_GROUP" nil nil t))) - -(defun ennu-html-inner-template (contents info) - (concat - ;; Table of contents - (let ((depth (plist-get info :with-toc))) - (when depth (org-html-toc depth info))) - ;; Beginning of h-entry - "
" - ;; Title - (format "

%s

\n" - (org-export-data (plist-get info :title) info)) - ;; Author and date - (let ((author (when (plist-get info :with-author) - (plist-get info :author))) - (date (when (plist-get info :with-date) - (org-export-get-date info)))) - (when (or author date) - (xmlgen `(p "Published" - ,@(when author - `(" by " - (a :class "p-author h-card" - :href ,(ennu--absolute-uri "") - ,(car (plist-get info :author))))) - ,@(when date - `(" on " - (time :class "dt-published" - :datetime ,(org-export-get-date info "%Y-%m-%d 12:00:00") - ,(org-export-get-date info "%B %d, %Y")))))))) - ;; Interlanguage language links - (when-let (translations (plist-get info :translations)) - (format "

In other languages: %s

" - (mapconcat - (lambda (translation) - (let ((lang (ennu-post-language translation)) - (slug (ennu-post-slug translation))) - (replace-regexp-in-string - "Tags: %s

" - (mapconcat - (lambda (tag) - (replace-regexp-in-string - "
%s" - (org-export-data (plist-get info :summary) info)) - ;; Document contents - (format "
%s
" contents) - ;; Footnotes section - (org-html-footnote-section info) - "
")) - -(defun ennu-html-link (link desc info) - ;; We override the html link transcoder to handle image links - ;; differently. We cannot use the `:export' property of - ;; `org-link-parameters' since those functions cannot access the - ;; `info' communication channel. - (let ((path (org-element-property :path link))) - (pcase (org-element-property :type link) - ("image" - ;; Convert image links to file links, get them transcoded by - ;; `org-html-link' and then remove the file:// scheme from the - ;; URI. Finally insert the transcoded image link in a link to a - ;; larger image as specified by the :image-link-width setting. - (format "%s" - (expand-file-name* - (ennu-image-output-filename - path (ennu-setting :image-link-width)) - (ennu-setting :images-directory)) - (replace-regexp-in-string - (rx (group (or "src" "data")) "=\"file://") "\\1=\"" - (org-html-link - (org-element-put-property - (org-element-put-property - link :path (url-encode-url - (expand-file-name* - (ennu-image-output-filename - path (ennu-setting :default-image-width)) - (ennu-setting :images-directory)))) - :type "file") - desc info)))) - ;; Pass other link types to org-html-link - (_ (org-html-link link desc info))))) - -(defmacro ennu-follow (path) - `(ennu-with-current-directory (ennu-setting :working-directory) - (find-file ,path))) - -;; TODO: Pass title through org-export-data-with-backend or something -;; similar in order to export org syntax in title -(defun ennu-export-post (path desc backend) - (pcase backend - ((or 'ennu-html 'html) - (let ((filename (concat (expand-file-name path (ennu-setting :posts-directory)) - ".org"))) - (xmlgen `(a :href ,(url-encode-url - (expand-file-name* path (ennu-setting :posts-directory))) - ,(or desc (ennu-post-title (ennu-read-post filename))))))))) - -(defun ennu-follow-post (path) - (ennu-follow (expand-file-name (concat path ".org") - (ennu-setting :posts-directory)))) - -(org-link-set-parameters - "post" - :export 'ennu-export-post - :follow 'ennu-follow-post) - -(defun ennu-follow-image (path) - (ennu-follow (expand-file-name path (ennu-setting :images-directory)))) - -(org-link-set-parameters - "image" :follow 'ennu-follow-image) - -(defun ennu-export-thumbnail (path desc backend) - (pcase backend - ((or 'ennu-html 'html) - (xmlgen - `(img :src ,(url-encode-url - (expand-file-name* - (ennu-image-output-filename - path (ennu-setting :thumbnail-image-width)) - (ennu-setting :images-directory)))))))) - -(org-link-set-parameters - "thumbnail" - :export 'ennu-export-thumbnail - :follow 'ennu-follow-image) - -(defun ennu-export-video (path desc backend) - (pcase backend - ((or 'ennu-html 'html) - (let ((video-directory (ennu-setting :video-directory))) - (xmlgen - `(video :src ,(url-encode-url (expand-file-name* path video-directory)) - :poster ,(url-encode-url - (expand-file-name* (ennu-video-poster path) video-directory)) - :preload "none" - :controls "")))))) - -(org-link-set-parameters - "video" :export 'ennu-export-video) - -(defun ennu-export-static (path desc backend) - (pcase backend - ((or 'ennu-html 'html) - (xmlgen - `(a :href ,(url-encode-url - (expand-file-name* path (ennu-setting :static-directory))) - ,desc))))) - -(org-link-set-parameters - "static" :export 'ennu-export-static) - -(org-link-set-parameters - "tangle" :export 'ennu-export-static) - -(defun ennu-export-tag (tag desc backend) - (pcase backend - ((or 'ennu-html 'html) - (xmlgen - `(a :href ,(url-encode-url - (expand-file-name* tag (ennu-setting :tag-directory))) - ,(or desc tag)))))) - -(org-link-set-parameters - "tag" :export 'ennu-export-tag) - -(provide 'ennu-html) diff --git a/ennu-image.el b/ennu-image.el deleted file mode 100644 index 34c3e7e..0000000 --- a/ennu-image.el +++ /dev/null @@ -1,55 +0,0 @@ -;; -*- lexical-binding: t -*- - -(require 'image) -(require 'seq) - -;; Check if all necessary image types are supported -(seq-do (lambda (image-type) - (unless (image-type-available-p image-type) - (lwarn '(ennu) :error "`%s' image type not supported" image-type))) - '(jpeg png svg)) - -;; Check for existence of external image processing utilities -(seq-do (lambda (external-program) - (unless (executable-find external-program) - (lwarn '(ennu) :error "`%s' not found" external-program))) - '("convert" "identify" "jpegtran" "optipng")) - -(defun ennu-image-resize-image (infile-path outfile-path width) - "A simple shell wrapper around ImageMagick's convert" - (ennu-image--assert-file-exists infile-path) - (cl-case (image-type infile-path) - (svg - (copy-file infile-path outfile-path t)) - (otherwise - (call-process "convert" nil nil nil - infile-path "-resize" (format "%d>" width) outfile-path))) - outfile-path) - -(defun ennu-image-optimize-image (image-path) - "A simple shell wrapper around jpegtran and optipng" - (ennu-image--assert-file-exists image-path) - (cl-case (image-type image-path) - (jpeg - (call-process "jpegtran" nil nil nil "-optimize" - "-progressive" "-copy" "none" - "-outfile" image-path image-path)) - (png - (call-process "optipng" nil nil nil image-path))) - image-path) - -(defun ennu-image-get-width (image-path) - (ennu-image--assert-file-exists image-path) - (cl-case (image-type image-path) - (svg 1e+INF) - (otherwise - (with-temp-buffer - (call-process "identify" nil t nil - "-format" "%w" image-path) - (string-to-number (buffer-string)))))) - -(defun ennu-image--assert-file-exists (path) - (unless (file-exists-p path) - (error "File %s does not exist" path))) - -(provide 'ennu-image) diff --git a/ennu.el b/ennu.el deleted file mode 100644 index 439e1d9..0000000 --- a/ennu.el +++ /dev/null @@ -1,524 +0,0 @@ -;; -*- lexical-binding: t -*- - -(require 'ennu-html) -(require 'ennu-image) -(require 'ox) -(require 'seq) -(require 'cl) -(require 'map) -(require 'memoize) -(require 'simple-httpd) - -(defvar ennu-version "0.1.0" - "Ennu version string") - -(cl-defstruct (ennu-post (:constructor ennu-make-post) - (:copier nil)) - filename slug author date language links tangle - summary tags thumbnail title translation-group) - -(cl-defstruct (ennu-operation (:constructor ennu-make-operation) - (:copier nil)) - inputs outputs publish) - -(defun ennu-posts (posts-directory) - (sort (seq-map 'ennu-read-post - (file-expand-wildcards - (concat (file-name-as-directory - (ennu-setting :posts-directory)) - "*.org"))) - 'ennu-later-post-p)) - -(defun ennu-later-post-p (post1 post2) - (time-less-p (ennu-post-date post2) - (ennu-post-date post1))) - -(defun ennu-read-post (filename) - (ennu--read-post - filename (file-attribute-modification-time - (file-attributes filename)))) - -(defmemoize ennu--read-post (filename last-modified) - (ennu-with-file-contents filename - (let ((metadata (org-export-get-environment 'ennu-html)) - (export (apply-partially 'org-export-with-backend 'ennu-html))) - (seq-do (lambda (key) - (unless (plist-member metadata key) - (user-error "Metadata %s not specified" key))) - ennu-mandatory-metadata) - (let* ((tree (org-element-parse-buffer)) - (links (org-element-map tree 'link - (lambda (link) - (pcase link - (`(link ,properties . ,_) - (let ((link-type (org-element-property :type link))) - (when (member link-type (list "image" "static" "video")) - (cons link-type (org-element-property :path link)))))))))) - (ennu-make-post - :filename filename - :slug (file-name-base filename) - :author (when-let (author (plist-get metadata :author)) - (funcall export (first author))) - :date (org-timestamp-to-time (first (plist-get metadata :date))) - :language (plist-get metadata :language) - :links links - ;; TODO: Deal with cases when the :tangle parameter is "yes" - :tangle (seq-uniq - (org-element-map tree 'src-block - (lambda (src-block) - (pcase (org-babel-get-src-block-info nil src-block) - (`(,_ ,_ ,arguments ,_ ,_ ,_ ,_) - (let ((tangle-output-file (map-elt arguments :tangle))) - (pcase tangle-output-file - ("no" nil) - (_ tangle-output-file)))))))) - :summary (when-let (summary (plist-get metadata :summary)) - (funcall export (first summary))) - :tags (plist-get metadata :filetags) - :thumbnail (or (plist-get metadata :thumbnail) - (seq-some (lambda (link) - (pcase link - (`("image" . ,path) path) - (`("video" . ,path) (ennu-video-poster path)))) - links)) - :title (funcall export (first (plist-get metadata :title))) - :translation-group (or (plist-get metadata :translation-group) - (file-name-base filename))))))) - -(defvar ennu-mandatory-metadata - (list :title :date)) - -(defmacro ennu-with-file-contents (file &rest body) - "Create a temporary buffer, insert contents of FILE into that -buffer and evaluate BODY. The value returned is the value of the -last form in BODY." - (declare (indent defun)) - `(with-temp-buffer - (insert-file-contents ,file) - ,@body)) - -(defun ennu--org-output-filename (filename) - (concat (file-name-sans-extension filename) ".html")) - -(defun ennu-publish-post (posts) - (let ((link-publish-operations - (seq-mapcat 'ennu-publish-link (seq-mapcat 'ennu-post-links posts))) - (input-post-files (seq-map 'ennu-post-filename posts))) - (append - (list - (ennu-make-operation - :inputs (append input-post-files - (seq-mapcat 'ennu-operation-inputs link-publish-operations)) - :outputs (seq-map 'ennu--org-output-filename input-post-files) - :publish - (lambda (&rest output-files) - (seq-mapn - (lambda (post output-file) - (let ((system-time-locale (map-elt (ennu-setting :locale-alist) - (ennu-post-language post) nil 'string=))) - (ennu-with-file-contents (ennu-post-filename post) - (org-export-to-file - 'ennu-html output-file nil nil nil nil - (list :translations (seq-remove (apply-partially 'equal post) posts)))))) - posts - output-files)))) - (ennu--filter-map - (lambda (post) - (when (ennu-post-tangle post) - (ennu-make-operation - :inputs (list (ennu-post-filename post)) - :outputs (seq-map (lambda (tangle-output) - (ennu--expand-relative tangle-output - (ennu-setting :static-directory))) - (ennu-post-tangle post)) - :publish (lambda (&rest output-files) - ;; TODO: Handle tangle outputs that are nested - ;; into directories, and when each tangle output - ;; is nested into a different directory. - (let ((post-file-copy (concat - (file-name-directory (first output-files)) - (file-name-nondirectory (ennu-post-filename post))))) - (copy-file (ennu-post-filename post) post-file-copy) - (org-babel-tangle-file post-file-copy) - (delete-file post-file-copy)))))) - posts) - link-publish-operations))) - -(defun ennu-publish-generic (other-files-directory file) - (ennu-make-operation - :inputs (list file) - :outputs - (list (string-remove-prefix - (file-name-as-directory other-files-directory) - (pcase (file-name-extension file) - ("org" (ennu--org-output-filename file)) - (_ file)))) - :publish (lambda (output-file) - (pcase (file-name-extension file) - ("org" (ennu-with-file-contents file - (org-export-to-file 'html output-file))) - (_ (ennu-copy file output-file)))))) - -(defun ennu-video-poster (video) - (pcase (directory-files (ennu-setting :images-directory) nil - (concat (file-name-sans-extension video) - "\\.\\(jpg\\|png\\)$")) - (`(,poster . ,_) poster) - (`() (user-error "Poster for %s not found" video)))) - -(defun ennu-add-tongue-suffix (filename tongue) - (pcase tongue - ("en" filename) - (_ (format "%s.%s%s" - (file-name-sans-extension filename) - tongue - (file-name-extension filename t))))) - -(defun ennu-index-filename (filename-prefix tongue &optional extension page-number) - (let ((extension (if extension (concat "." extension) ""))) - (ennu-add-tongue-suffix - (if page-number - (format "%s-%s%s" filename-prefix page-number extension) - (concat filename-prefix extension)) - tongue))) - -(defun ennu-publish-index (filename-prefix title posts-per-page posts) - (let* ((tongue (ennu-post-language (first posts))) - (number-of-pages (ceiling (length posts) posts-per-page)) - (page-numbers (number-sequence 1 number-of-pages))) - (ennu-make-operation - :inputs (seq-map 'ennu-post-filename posts) - :outputs (cons (ennu-add-tongue-suffix (format "%s.html" filename-prefix) tongue) - (seq-map (apply-partially 'ennu-index-filename filename-prefix tongue "html") - page-numbers)) - :publish - (lambda (home-page &rest output-files) - (let ((system-time-locale (map-elt (ennu-setting :locale-alist) tongue nil 'string=))) - (seq-mapn - (lambda (posts page-number output-file) - (with-temp-buffer - (insert (format "#+TITLE: %s\n" title)) - (insert (format "#+LANGUAGE: %s\n" tongue)) - (insert "#+OPTIONS: num:nil toc:nil\n\n") - (seq-do (lambda (post) - (insert (format "* [[post:%s]]\n" (ennu-post-slug post))) - (insert (format-time-string "/%b %e, %Y/\n\n" (ennu-post-date post))) - (when-let ((thumbnail (ennu-post-thumbnail post))) - (insert (format "[[thumbnail:%s]]\n\n" thumbnail))) - (when-let ((summary (ennu-post-summary post))) - (insert summary) - (insert "\n\n")) - (when-let ((tags (ennu-post-tags post))) - (insert "Tags: ") - (insert - (string-join - (seq-map (lambda (tag) - (format "[[tag:%s][%s]]" (ennu-add-tongue-suffix tag tongue) tag)) - tags) - ", ")) - (insert "\n\n"))) - posts) - (unless (= page-number 1) - (insert (format "[[./%s][Newer posts]]\n" - (ennu-index-filename (file-name-nondirectory filename-prefix) - tongue nil (1- page-number))))) - (unless (= page-number number-of-pages) - (insert (format "[[./%s][Older posts]]\n" - (ennu-index-filename (file-name-nondirectory filename-prefix) - tongue nil (1+ page-number))))) - (org-export-to-file 'html output-file))) - (seq-partition posts posts-per-page) - page-numbers - output-files)) - (copy-file (first output-files) home-page))))) - -(defun ennu--absolute-uri (path) - (format "%s://%s/%s" - (ennu-setting :blog-scheme) - (ennu-setting :blog-domain) - path)) - -(defun ennu--atom-date (date) - (format-time-string "%Y-%m-%dT%H:%M:%SZ" date)) - -(defun ennu-publish-feed (feed-file title rights posts) - (ennu-make-operation - :inputs (seq-map 'ennu-post-filename posts) - :outputs (list feed-file) - :publish - (lambda (output-file) - (with-temp-file output-file - (insert - (xmlgen - `(feed :xmlns "http://www.w3.org/2005/Atom" - (id ,(ennu--absolute-uri "")) - (title ,title) - (updated ,(ennu--atom-date (ennu-post-date (first posts)))) - (link :rel "self" :href ,(ennu--absolute-uri feed-file)) - (generator - ,(format "Emacs %d.%d Org-mode %s ennu %s" - emacs-major-version emacs-minor-version (org-version) ennu-version)) - (rights ,rights) - ,@(seq-map 'ennu--feed-entry posts)))))))) - -(defun ennu--feed-entry (post) - (let ((link (ennu--absolute-uri (ennu--org-output-filename - (ennu-post-filename post))))) - `(entry (id ,link) - (title :xml:lang ,(ennu-post-language post) ,(ennu-post-title post)) - (updated ,(ennu--atom-date (ennu-post-date post))) - ,@(when org-export-with-author - `((author - (name ,(ennu-post-author post)) - (email ,user-mail-address)))) - (content :type "html" :xml:lang ,(ennu-post-language post) - ,(ennu-with-file-contents (ennu-post-filename post) - (org-export-as 'ennu-html nil nil t))) - (link :rel "alternate" :href ,link) - ,@(seq-map (lambda (tag) `(category :term ,tag)) - (ennu-post-tags post))))) - -(defun ennu-setting (property) - (pcase property - ((or :blog-domain :blog-license :blog-title - :images-directory :output-directory :posts-directory - :static-directory :tag-directory :video-directory - :working-directory) - (or (plist-get ennu-blog property) - (user-error "Property %s not defined" property))) - ((or :atom-feed-number-of-posts :atom-feed-file - :blog-scheme :default-image-width - :image-link-width :index-posts-per-page - :locale-alist :other-files-directory - :tag-directory :thumbnail-image-width) - (plist-get (org-combine-plists - (list :atom-feed-number-of-posts 12 - :atom-feed-file "blog.atom" - :blog-scheme "https" - :default-image-width 640 - :image-link-width 1024 - :index-posts-per-page 12 - :locale-alist '(("en" . "C")) - :tag-directory "tag" - :thumbnail-image-width 320) - ennu-blog) - property)) - (_ (error "Unknown property %s" property)))) - -(defun ennu-image-output-filename (image width) - (format "%s-%spx.%s" - (file-name-sans-extension image) - width (file-name-extension image))) - -(defun ennu--expand-relative (name directory) - (concat (file-name-as-directory directory) name)) - -(defun ennu-publish-image (widths image) - (ennu-make-operation - :inputs (list image) - :outputs (seq-map (apply-partially 'ennu-image-output-filename image) - widths) - :publish - (lambda (&rest output-files) - (seq-mapn (lambda (output-file width) - (ennu-image-optimize-image - (ennu-image-resize-image image output-file width))) - output-files widths)))) - -(defun ennu-publish-copy (file) - (ennu-make-operation - :inputs (list file) - :outputs (list file) - :publish (apply-partially 'ennu-copy file))) - -(defun newest-file (files) - (pcase files - (`(,head . ,tail) - (seq-reduce (lambda (file1 file2) - (if (file-newer-than-file-p file1 file2) - file1 file2)) - tail head)))) - -(defun ennu-mkdir-p (directory) - (make-directory directory t)) - -(defun ennu-copy (source destination) - "Copy file or directory from SOURCE to DESTINATION. Overwrite -if DESTINATION already exists." - (if (file-directory-p source) - (copy-directory source destination) - (make-directory (file-name-directory destination) t) - (copy-file source destination t))) - -(defun ennu--filter-map (function sequence) - (seq-filter 'identity (seq-map function sequence))) - -(defun ennu--do-operation (temporary-directory operation) - (let* ((expand (lambda (directory file) - (expand-file-name file directory))) - (inputs (ennu-operation-inputs operation)) - (outputs (ennu-operation-outputs operation)) - (absolute-outputs - (seq-map (apply-partially expand temporary-directory) - outputs)) - (previous-outputs - (seq-map (apply-partially expand (ennu-setting :output-directory)) - outputs))) - (cond - ((and (seq-every-p 'file-exists-p previous-outputs) - (file-newer-than-file-p (newest-file previous-outputs) - (newest-file inputs))) - (message "Skipping publishing %s to %s" inputs outputs) - (seq-mapn 'ennu-copy previous-outputs absolute-outputs)) - (t (message "Publishing %s to %s" inputs outputs) - (seq-do 'ennu-mkdir-p - (seq-uniq - (seq-map 'file-name-directory absolute-outputs))) - (apply (ennu-operation-publish operation) absolute-outputs))))) - -(defun ennu-publish-static-file (file) - (ennu-make-operation - :inputs (list file) - :outputs (list file) - :publish (apply-partially 'ennu-copy file))) - -(defun ennu-publish-link (link) - (pcase link - (`("image" . ,path) - (list - (ennu-publish-image - (list (ennu-setting :default-image-width) - (ennu-setting :image-link-width)) - (ennu--expand-relative path (ennu-setting :images-directory))))) - (`("static" . ,path) - (list - (ennu-publish-copy (ennu--expand-relative path (ennu-setting :static-directory))))) - (`("video" . ,path) - (list - (ennu-publish-copy (ennu--expand-relative path (ennu-setting :video-directory))) - (ennu-publish-copy (ennu--expand-relative (ennu-video-poster path) - (ennu-setting :images-directory))))))) - -(defmacro ennu-with-current-directory (directory &rest body) - "Change to DIRECTORY, evaluate BODY and restore the current -working directory. The value returned is the value of the last -form in BODY." - (declare (indent defun)) - (let ((current-directory-symbol (make-symbol "current-directory"))) - `(let ((,current-directory-symbol default-directory)) - (unwind-protect (progn (cd ,directory) ,@body) - (cd ,current-directory-symbol))))) - -(defmacro ennu-with-temporary-directory (temporary-directory &rest body) - "Create temporary directory, evaluate BODY with the absolute -path of that directory assigned to TEMPORARY-DIRECTORY and -finally delete the temporary directory. The value returned is the -value of the last form in BODY." - (declare (indent defun)) - `(let ((,temporary-directory (make-temp-file "ennu" t))) - (chmod ,temporary-directory #o755) - (unwind-protect - (progn ,@body) - (delete-directory ,temporary-directory t)))) - -(defun ennu-many-to-many-group-by (function sequence) - "Apply FUNCTION to each element of SEQUENCE. -Separate the elements of SEQUENCE into an alist using the results -as keys. Keys are compared using `equal'." - (seq-reduce - (lambda (result element) - (seq-do - (lambda (key) - (map-put result key - (cons element (map-elt result key nil 'equal)) - 'equal)) - (funcall function element)) - result) - (seq-reverse sequence) - nil)) - -(defun ennu-publish () - (interactive) - (let ((make-backup-files nil) - (blog-title (ennu-setting :blog-title)) - (posts-per-page (ennu-setting :index-posts-per-page))) - (ennu-with-current-directory (ennu-setting :working-directory) - (ennu-with-temporary-directory temporary-directory - (seq-do - (apply-partially 'ennu--do-operation temporary-directory) - (append - (let ((posts (ennu-posts (ennu-setting :posts-directory)))) - (append - ;; Publish posts - (seq-mapcat (pcase-lambda (`(,translation-group . ,posts)) - (ennu-publish-post posts)) - (seq-group-by 'ennu-post-translation-group posts)) - ;; Publish feed - (list (ennu-publish-feed (ennu-setting :atom-feed-file) - blog-title - (ennu-setting :blog-license) - (seq-take posts (ennu-setting :atom-feed-number-of-posts)))) - ;; Publish indices - (seq-map - (pcase-lambda (`(,tongue . ,posts)) - (ennu-publish-index "index" blog-title posts-per-page posts)) - (seq-group-by 'ennu-post-language posts)) - (seq-mapcat - (pcase-lambda (`(,tag . ,posts)) - (seq-map - (pcase-lambda (`(,tongue . ,posts)) - (ennu-publish-index - (ennu--expand-relative tag (ennu-setting :tag-directory)) - tag posts-per-page posts)) - (seq-group-by 'ennu-post-language posts))) - (ennu-many-to-many-group-by 'ennu-post-tags posts)) - ;; Publish thumbnails - (seq-map - (apply-partially 'ennu-publish-image (list (ennu-setting :thumbnail-image-width))) - (seq-map (lambda (image) - (ennu--expand-relative image (ennu-setting :images-directory))) - (seq-uniq (ennu--filter-map 'ennu-post-thumbnail posts)))))) - ;; Publish other files - (when-let ((other-files-directory (ennu-setting :other-files-directory))) - (seq-map (apply-partially 'ennu-publish-generic other-files-directory) - (seq-map (apply-partially 'string-remove-prefix - (file-name-as-directory (expand-file-name default-directory))) - (directory-files-recursively other-files-directory ".")))))) - ;; Replace old output directory - (let ((output (ennu-setting :output-directory))) - (delete-directory output t) - (rename-file temporary-directory output t)))))) - -;;; Server -;;; -;;; Test HTTP server to serve the blog locally - -(defun ennu-server-start () - (interactive) - (setq httpd-root (expand-file-name (ennu-setting :output-directory) - (ennu-setting :working-directory))) - (defun httpd/ (proc uri-path query request) - (let* ((uri-path (httpd-unhex uri-path)) - (file-path (httpd-gen-path uri-path))) - (cond - ;; If a HTML file other than index.html was requested, reject - ;; that request. - ((and (not (string= (file-name-nondirectory file-path) "index.html")) - (string= (file-name-extension file-path) "html")) - (httpd-error proc 404)) - ;; If the requested file was found, serve it. - ((= (httpd-status file-path) 200) - (httpd-serve-root proc httpd-root uri-path request)) - ;; Perhaps, this is a post or other HTML file that is being - ;; requested. Try serving a file with a .html extension - ;; appended. - (t (httpd-serve-root proc httpd-root (concat uri-path ".html") request))))) - (httpd-start) - (message "Ennu web server listening at http://localhost:%d" httpd-port)) - -(defun ennu-server-stop () - (interactive) - (httpd-stop) - (message "Ennu web server stopped")) - -(provide 'ennu) diff --git a/ennum-html.el b/ennum-html.el new file mode 100644 index 0000000..1475927 --- /dev/null +++ b/ennum-html.el @@ -0,0 +1,383 @@ +;; -*- lexical-binding: t -*- + +(require 'ox) +(require 'subr-x) +(require 'xmlgen) + +(defconst ennum--iso-639-1-alist + '(("ab" . "аҧсуа бызшәа, аҧсшәа") + ("aa" . "Afaraf") + ("af" . "Afrikaans") + ("ak" . "Akan") + ("sq" . "Shqip") + ("am" . "አማርኛ") + ("ar" . "العربية") + ("an" . "aragonés") + ("hy" . "Հայերեն") + ("as" . "অসমীয়া") + ("av" . "авар мацӀ, магӀарул мацӀ") + ("ae" . "avesta") + ("ay" . "aymar aru") + ("az" . "azərbaycan dili") + ("bm" . "bamanankan") + ("ba" . "башҡорт теле") + ("eu" . "euskara, euskera") + ("be" . "беларуская мова") + ("bn" . "বাংলা") + ("bh" . "भोजपुरी") + ("bi" . "Bislama") + ("bs" . "bosanski jezik") + ("br" . "brezhoneg") + ("bg" . "български език") + ("my" . "ဗမာစာ") + ("ca" . "català") + ("ch" . "Chamoru") + ("ce" . "нохчийн мотт") + ("ny" . "chiCheŵa, chinyanja") + ("zh" . "中文 (Zhōngwén), 汉语, 漢語") + ("cv" . "чӑваш чӗлхи") + ("kw" . "Kernewek") + ("co" . "corsu, lingua corsa") + ("cr" . "ᓀᐦᐃᔭᐍᐏᐣ") + ("hr" . "hrvatski jezik") + ("cs" . "čeština, český jazyk") + ("da" . "dansk") + ("dv" . "ދިވެހި") + ("nl" . "Nederlands, Vlaams") + ("dz" . "རྫོང་ཁ") + ("en" . "English") + ("eo" . "Esperanto") + ("et" . "eesti, eesti keel") + ("ee" . "Eʋegbe") + ("fo" . "føroyskt") + ("fj" . "vosa Vakaviti") + ("fi" . "suomi, suomen kieli") + ("fr" . "français, langue française") + ("ff" . "Fulfulde, Pulaar, Pular") + ("gl" . "galego") + ("ka" . "ქართული") + ("de" . "Deutsch") + ("el" . "ελληνικά") + ("gn" . "Avañe'ẽ") + ("gu" . "ગુજરાતી") + ("ht" . "Kreyòl ayisyen") + ("ha" . "(Hausa) هَوُسَ") + ("he" . "עברית") + ("hz" . "Otjiherero") + ("hi" . "हिन्दी, हिंदी") + ("ho" . "Hiri Motu") + ("hu" . "magyar") + ("ia" . "Interlingua") + ("id" . "Bahasa Indonesia") + ("ie" . "Originally called Occidental; then Interlingue after WWII") + ("ga" . "Gaeilge") + ("ig" . "Asụsụ Igbo") + ("ik" . "Iñupiaq, Iñupiatun") + ("io" . "Ido") + ("is" . "Íslenska") + ("it" . "Italiano") + ("iu" . "ᐃᓄᒃᑎᑐᑦ") + ("ja" . "日本語 (にほんご)") + ("jv" . "ꦧꦱꦗꦮ, Basa Jawa") + ("kl" . "kalaallisut, kalaallit oqaasii") + ("kn" . "ಕನ್ನಡ") + ("kr" . "Kanuri") + ("ks" . "कश्मीरी, كشميري‎") + ("kk" . "қазақ тілі") + ("km" . "ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ") + ("ki" . "Gĩkũyũ") + ("rw" . "Ikinyarwanda") + ("ky" . "Кыргызча, Кыргыз тили") + ("kv" . "коми кыв") + ("kg" . "Kikongo") + ("ko" . "한국어") + ("ku" . "Kurdî, كوردی‎") + ("kj" . "Kuanyama") + ("la" . "latine, lingua latina") + ("lb" . "Lëtzebuergesch") + ("lg" . "Luganda") + ("li" . "Limburgs") + ("ln" . "Lingála") + ("lo" . "ພາສາລາວ") + ("lt" . "lietuvių kalba") + ("lu" . "Tshiluba") + ("lv" . "latviešu valoda") + ("gv" . "Gaelg, Gailck") + ("mk" . "македонски јазик") + ("mg" . "fiteny malagasy") + ("ms" . "bahasa Melayu, بهاس ملايو‎") + ("ml" . "മലയാളം") + ("mt" . "Malti") + ("mi" . "te reo Māori") + ("mr" . "मराठी") + ("mh" . "Kajin M̧ajeļ") + ("mn" . "Монгол хэл") + ("na" . "Dorerin Naoero") + ("nv" . "Diné bizaad") + ("nd" . "isiNdebele") + ("ne" . "नेपाली") + ("ng" . "Owambo") + ("nb" . "Norsk bokmål") + ("nn" . "Norsk nynorsk") + ("no" . "Norsk") + ("ii" . "ꆈꌠ꒿ Nuosuhxop") + ("nr" . "isiNdebele") + ("oc" . "occitan, lenga d'òc") + ("oj" . "ᐊᓂᔑᓈᐯᒧᐎᓐ") + ("cu" . "ѩзыкъ словѣньскъ") + ("om" . "Afaan Oromoo") + ("or" . "ଓଡ଼ିଆ") + ("os" . "ирон æвзаг") + ("pa" . "ਪੰਜਾਬੀ") + ("pi" . "पाऴि") + ("fa" . "فارسی") + ("pl" . "język polski, polszczyzna") + ("ps" . "پښتو") + ("pt" . "Português") + ("qu" . "Runa Simi, Kichwa") + ("rm" . "rumantsch grischun") + ("rn" . "Ikirundi") + ("ro" . "Română") + ("ru" . "Русский") + ("sa" . "संस्कृतम्") + ("sc" . "sardu") + ("sd" . "सिन्धी, سنڌي، سندھی‎") + ("se" . "Davvisámegiella") + ("sm" . "gagana fa'a Samoa") + ("sg" . "yângâ tî sängö") + ("sr" . "српски језик") + ("gd" . "Gàidhlig") + ("sn" . "chiShona") + ("si" . "සිංහල") + ("sk" . "slovenčina, slovenský jazyk") + ("sl" . "slovenski jezik, slovenščina") + ("so" . "Soomaaliga, af Soomaali") + ("st" . "Sesotho") + ("es" . "Español") + ("su" . "Basa Sunda") + ("sw" . "Kiswahili") + ("ss" . "SiSwati") + ("sv" . "svenska") + ("ta" . "தமிழ்") + ("te" . "తెలుగు") + ("tg" . "тоҷикӣ, toçikī, تاجیکی‎") + ("th" . "ไทย") + ("ti" . "ትግርኛ") + ("bo" . "བོད་ཡིག") + ("tk" . "Türkmen, Түркмен") + ("tl" . "Wikang Tagalog") + ("tn" . "Setswana") + ("to" . "faka Tonga") + ("tr" . "Türkçe") + ("ts" . "Xitsonga") + ("tt" . "татар теле, tatar tele") + ("tw" . "Twi") + ("ty" . "Reo Tahiti") + ("ug" . "ئۇيغۇرچە‎, Uyghurche") + ("uk" . "Українська") + ("ur" . "اردو") + ("uz" . "Oʻzbek, Ўзбек, أۇزبېك‎") + ("ve" . "Tshivenḓa") + ("vi" . "Tiếng Việt") + ("vo" . "Volapük") + ("wa" . "walon") + ("cy" . "Cymraeg") + ("wo" . "Wollof") + ("fy" . "Frysk") + ("xh" . "isiXhosa") + ("yi" . "ייִדיש") + ("yo" . "Yorùbá") + ("za" . "Saɯ cueŋƅ, Saw cuengh") + ("zu" . "isiZulu"))) + +(defun expand-file-name* (name default-directory) + (expand-file-name name (concat "/" default-directory))) + +(org-export-define-derived-backend 'ennum-html 'html + :translate-alist + '((inner-template . ennum-html-inner-template) + (link . ennum-html-link)) + :options-alist + '((:summary "SUMMARY" nil nil parse) + (:thumbnail "THUMBNAIL" nil nil t) + (:translation-group "TRANSLATION_GROUP" nil nil t))) + +(defun ennum-html-inner-template (contents info) + (concat + ;; Table of contents + (let ((depth (plist-get info :with-toc))) + (when depth (org-html-toc depth info))) + ;; Beginning of h-entry + "
" + ;; Title + (format "

%s

\n" + (org-export-data (plist-get info :title) info)) + ;; Author and date + (let ((author (when (plist-get info :with-author) + (plist-get info :author))) + (date (when (plist-get info :with-date) + (org-export-get-date info)))) + (when (or author date) + (xmlgen `(p "Published" + ,@(when author + `(" by " + (a :class "p-author h-card" + :href ,(ennum--absolute-uri "") + ,(car (plist-get info :author))))) + ,@(when date + `(" on " + (time :class "dt-published" + :datetime ,(org-export-get-date info "%Y-%m-%d 12:00:00") + ,(org-export-get-date info "%B %d, %Y")))))))) + ;; Interlanguage language links + (when-let (translations (plist-get info :translations)) + (format "

In other languages: %s

" + (mapconcat + (lambda (translation) + (let ((lang (ennum-post-language translation)) + (slug (ennum-post-slug translation))) + (replace-regexp-in-string + "Tags: %s

" + (mapconcat + (lambda (tag) + (replace-regexp-in-string + "
%s" + (org-export-data (plist-get info :summary) info)) + ;; Document contents + (format "
%s
" contents) + ;; Footnotes section + (org-html-footnote-section info) + "
")) + +(defun ennum-html-link (link desc info) + ;; We override the html link transcoder to handle image links + ;; differently. We cannot use the `:export' property of + ;; `org-link-parameters' since those functions cannot access the + ;; `info' communication channel. + (let ((path (org-element-property :path link))) + (pcase (org-element-property :type link) + ("image" + ;; Convert image links to file links, get them transcoded by + ;; `org-html-link' and then remove the file:// scheme from the + ;; URI. Finally insert the transcoded image link in a link to a + ;; larger image as specified by the :image-link-width setting. + (format "%s" + (expand-file-name* + (ennum-image-output-filename + path (ennum-setting :image-link-width)) + (ennum-setting :images-directory)) + (replace-regexp-in-string + (rx (group (or "src" "data")) "=\"file://") "\\1=\"" + (org-html-link + (org-element-put-property + (org-element-put-property + link :path (url-encode-url + (expand-file-name* + (ennum-image-output-filename + path (ennum-setting :default-image-width)) + (ennum-setting :images-directory)))) + :type "file") + desc info)))) + ;; Pass other link types to org-html-link + (_ (org-html-link link desc info))))) + +(defmacro ennum-follow (path) + `(ennum-with-current-directory (ennum-setting :working-directory) + (find-file ,path))) + +;; TODO: Pass title through org-export-data-with-backend or something +;; similar in order to export org syntax in title +(defun ennum-export-post (path desc backend) + (pcase backend + ((or 'ennum-html 'html) + (let ((filename (concat (expand-file-name path (ennum-setting :posts-directory)) + ".org"))) + (xmlgen `(a :href ,(url-encode-url + (expand-file-name* path (ennum-setting :posts-directory))) + ,(or desc (ennum-post-title (ennum-read-post filename))))))))) + +(defun ennum-follow-post (path) + (ennum-follow (expand-file-name (concat path ".org") + (ennum-setting :posts-directory)))) + +(org-link-set-parameters + "post" + :export 'ennum-export-post + :follow 'ennum-follow-post) + +(defun ennum-follow-image (path) + (ennum-follow (expand-file-name path (ennum-setting :images-directory)))) + +(org-link-set-parameters + "image" :follow 'ennum-follow-image) + +(defun ennum-export-thumbnail (path desc backend) + (pcase backend + ((or 'ennum-html 'html) + (xmlgen + `(img :src ,(url-encode-url + (expand-file-name* + (ennum-image-output-filename + path (ennum-setting :thumbnail-image-width)) + (ennum-setting :images-directory)))))))) + +(org-link-set-parameters + "thumbnail" + :export 'ennum-export-thumbnail + :follow 'ennum-follow-image) + +(defun ennum-export-video (path desc backend) + (pcase backend + ((or 'ennum-html 'html) + (let ((video-directory (ennum-setting :video-directory))) + (xmlgen + `(video :src ,(url-encode-url (expand-file-name* path video-directory)) + :poster ,(url-encode-url + (expand-file-name* (ennum-video-poster path) video-directory)) + :preload "none" + :controls "")))))) + +(org-link-set-parameters + "video" :export 'ennum-export-video) + +(defun ennum-export-static (path desc backend) + (pcase backend + ((or 'ennum-html 'html) + (xmlgen + `(a :href ,(url-encode-url + (expand-file-name* path (ennum-setting :static-directory))) + ,desc))))) + +(org-link-set-parameters + "static" :export 'ennum-export-static) + +(org-link-set-parameters + "tangle" :export 'ennum-export-static) + +(defun ennum-export-tag (tag desc backend) + (pcase backend + ((or 'ennum-html 'html) + (xmlgen + `(a :href ,(url-encode-url + (expand-file-name* tag (ennum-setting :tag-directory))) + ,(or desc tag)))))) + +(org-link-set-parameters + "tag" :export 'ennum-export-tag) + +(provide 'ennum-html) diff --git a/ennum-image.el b/ennum-image.el new file mode 100644 index 0000000..3a8aa23 --- /dev/null +++ b/ennum-image.el @@ -0,0 +1,55 @@ +;; -*- lexical-binding: t -*- + +(require 'image) +(require 'seq) + +;; Check if all necessary image types are supported +(seq-do (lambda (image-type) + (unless (image-type-available-p image-type) + (lwarn '(ennum) :error "`%s' image type not supported" image-type))) + '(jpeg png svg)) + +;; Check for existence of external image processing utilities +(seq-do (lambda (external-program) + (unless (executable-find external-program) + (lwarn '(ennum) :error "`%s' not found" external-program))) + '("convert" "identify" "jpegtran" "optipng")) + +(defun ennum-image-resize-image (infile-path outfile-path width) + "A simple shell wrapper around ImageMagick's convert" + (ennum-image--assert-file-exists infile-path) + (cl-case (image-type infile-path) + (svg + (copy-file infile-path outfile-path t)) + (otherwise + (call-process "convert" nil nil nil + infile-path "-resize" (format "%d>" width) outfile-path))) + outfile-path) + +(defun ennum-image-optimize-image (image-path) + "A simple shell wrapper around jpegtran and optipng" + (ennum-image--assert-file-exists image-path) + (cl-case (image-type image-path) + (jpeg + (call-process "jpegtran" nil nil nil "-optimize" + "-progressive" "-copy" "none" + "-outfile" image-path image-path)) + (png + (call-process "optipng" nil nil nil image-path))) + image-path) + +(defun ennum-image-get-width (image-path) + (ennum-image--assert-file-exists image-path) + (cl-case (image-type image-path) + (svg 1e+INF) + (otherwise + (with-temp-buffer + (call-process "identify" nil t nil + "-format" "%w" image-path) + (string-to-number (buffer-string)))))) + +(defun ennum-image--assert-file-exists (path) + (unless (file-exists-p path) + (error "File %s does not exist" path))) + +(provide 'ennum-image) diff --git a/ennum.el b/ennum.el new file mode 100644 index 0000000..69a7b5b --- /dev/null +++ b/ennum.el @@ -0,0 +1,534 @@ +;; -*- lexical-binding: t -*- + +(require 'ennum-html) +(require 'ennum-image) +(require 'ox) +(require 'seq) +(require 'cl) +(require 'map) +(require 'memoize) +(require 'simple-httpd) + +(defvar ennum-version "0.1.0" + "Ennum version string") + +(cl-defstruct (ennum-post (:constructor ennum-make-post) + (:copier nil)) + filename slug author date language links tangle + summary tags thumbnail title translation-group) + +(cl-defstruct (ennum-operation (:constructor ennum-make-operation) + (:copier nil)) + inputs outputs publish) + +(defun ennum-posts (posts-directory) + (sort (seq-map 'ennum-read-post + (file-expand-wildcards + (concat (file-name-as-directory + (ennum-setting :posts-directory)) + "*.org"))) + 'ennum-later-post-p)) + +(defun ennum-later-post-p (post1 post2) + (time-less-p (ennum-post-date post2) + (ennum-post-date post1))) + +(defun ennum-read-post (filename) + (ennum--read-post + filename (file-attribute-modification-time + (file-attributes filename)))) + +(defmemoize ennum--read-post (filename last-modified) + (ennum-with-file-contents filename + (let ((metadata (org-export-get-environment 'ennum-html)) + (export (apply-partially 'org-export-with-backend 'ennum-html))) + (seq-do (lambda (key) + (unless (plist-member metadata key) + (user-error "Metadata %s not specified" key))) + ennum-mandatory-metadata) + (let* ((tree (org-element-parse-buffer)) + (links (org-element-map tree 'link + (lambda (link) + (pcase link + (`(link ,properties . ,_) + (let ((link-type (org-element-property :type link))) + (when (member link-type (list "image" "static" "video")) + (cons link-type (org-element-property :path link)))))))))) + (ennum-make-post + :filename filename + :slug (file-name-base filename) + :author (when-let (author (plist-get metadata :author)) + (funcall export (first author))) + :date (org-timestamp-to-time (first (plist-get metadata :date))) + :language (plist-get metadata :language) + :links links + ;; TODO: Deal with cases when the :tangle parameter is "yes" + :tangle (seq-uniq + (org-element-map tree 'src-block + (lambda (src-block) + (pcase (org-babel-get-src-block-info nil src-block) + (`(,_ ,_ ,arguments ,_ ,_ ,_ ,_) + (let ((tangle-output-file (map-elt arguments :tangle))) + (pcase tangle-output-file + ("no" nil) + (_ tangle-output-file)))))))) + :summary (when-let (summary (plist-get metadata :summary)) + (funcall export (first summary))) + :tags (plist-get metadata :filetags) + :thumbnail (or (plist-get metadata :thumbnail) + (seq-some (lambda (link) + (pcase link + (`("image" . ,path) path) + (`("video" . ,path) (ennum-video-poster path)))) + links)) + :title (funcall export (first (plist-get metadata :title))) + :translation-group (or (plist-get metadata :translation-group) + (file-name-base filename))))))) + +(defvar ennum-mandatory-metadata + (list :title :date)) + +(defmacro ennum-with-file-contents (file &rest body) + "Create a temporary buffer, insert contents of FILE into that +buffer and evaluate BODY. The value returned is the value of the +last form in BODY." + (declare (indent defun)) + `(with-temp-buffer + (insert-file-contents ,file) + ,@body)) + +(defun ennum--org-output-filename (filename) + (concat (file-name-sans-extension filename) ".html")) + +(defun ennum-publish-post (posts) + (let ((link-publish-operations + (seq-mapcat 'ennum-publish-link (seq-mapcat 'ennum-post-links posts))) + (input-post-files (seq-map 'ennum-post-filename posts))) + (append + (list + (ennum-make-operation + :inputs (append input-post-files + (seq-mapcat 'ennum-operation-inputs link-publish-operations)) + :outputs (seq-map 'ennum--org-output-filename input-post-files) + :publish + (lambda (&rest output-files) + (seq-mapn + (lambda (post output-file) + (let ((system-time-locale (map-elt (ennum-setting :locale-alist) + (ennum-post-language post) nil 'string=))) + (ennum-with-file-contents (ennum-post-filename post) + (org-export-to-file + 'ennum-html output-file nil nil nil nil + (list :translations (seq-remove (apply-partially 'equal post) posts)))))) + posts + output-files)))) + (ennum--filter-map + (lambda (post) + (when (ennum-post-tangle post) + (ennum-make-operation + :inputs (list (ennum-post-filename post)) + :outputs (seq-map (lambda (tangle-output) + (ennum--expand-relative tangle-output + (ennum-setting :static-directory))) + (ennum-post-tangle post)) + :publish (lambda (&rest output-files) + ;; TODO: Handle tangle outputs that are nested + ;; into directories, and when each tangle output + ;; is nested into a different directory. + (let ((post-file-copy (concat + (file-name-directory (first output-files)) + (file-name-nondirectory (ennum-post-filename post))))) + (copy-file (ennum-post-filename post) post-file-copy) + (org-babel-tangle-file post-file-copy) + (delete-file post-file-copy)))))) + posts) + link-publish-operations))) + +(defun ennum-publish-generic (other-files-directory file) + (ennum-make-operation + :inputs (list file) + :outputs + (list (string-remove-prefix + (file-name-as-directory other-files-directory) + (pcase (file-name-extension file) + ("org" (ennum--org-output-filename file)) + (_ file)))) + :publish (lambda (output-file) + (pcase (file-name-extension file) + ("org" (ennum-with-file-contents file + (org-export-to-file 'html output-file))) + (_ (ennum-copy file output-file)))))) + +(defun ennum-video-poster (video) + (pcase (directory-files (ennum-setting :images-directory) nil + (concat (file-name-sans-extension video) + "\\.\\(jpg\\|png\\)$")) + (`(,poster . ,_) poster) + (`() (user-error "Poster for %s not found" video)))) + +(defun ennum-add-tongue-suffix (filename tongue) + (pcase tongue + ("en" filename) + (_ (format "%s.%s%s" + (file-name-sans-extension filename) + tongue + (file-name-extension filename t))))) + +(defun ennum-index-filename (filename-prefix tongue &optional extension page-number) + (let ((extension (if extension (concat "." extension) ""))) + (ennum-add-tongue-suffix + (if page-number + (format "%s-%s%s" filename-prefix page-number extension) + (concat filename-prefix extension)) + tongue))) + +(defun ennum-publish-index (filename-prefix title posts-per-page posts) + (let* ((tongue (ennum-post-language (first posts))) + (number-of-pages (ceiling (length posts) posts-per-page)) + (page-numbers (number-sequence 1 number-of-pages))) + (ennum-make-operation + :inputs (seq-map 'ennum-post-filename posts) + :outputs (cons (ennum-add-tongue-suffix (format "%s.html" filename-prefix) tongue) + (seq-map (apply-partially 'ennum-index-filename filename-prefix tongue "html") + page-numbers)) + :publish + (lambda (home-page &rest output-files) + (let ((system-time-locale (map-elt (ennum-setting :locale-alist) tongue nil 'string=))) + (seq-mapn + (lambda (posts page-number output-file) + (with-temp-buffer + (insert (format "#+TITLE: %s\n" title)) + (insert (format "#+LANGUAGE: %s\n" tongue)) + (insert "#+OPTIONS: num:nil toc:nil\n\n") + (seq-do (lambda (post) + (insert (format "* [[post:%s]]\n" (ennum-post-slug post))) + (insert (format-time-string "/%b %e, %Y/\n\n" (ennum-post-date post))) + (when-let ((thumbnail (ennum-post-thumbnail post))) + (insert (format "[[thumbnail:%s]]\n\n" thumbnail))) + (when-let ((summary (ennum-post-summary post))) + (insert summary) + (insert "\n\n")) + (when-let ((tags (ennum-post-tags post))) + (insert "Tags: ") + (insert + (string-join + (seq-map (lambda (tag) + (format "[[tag:%s][%s]]" (ennum-add-tongue-suffix tag tongue) tag)) + tags) + ", ")) + (insert "\n\n"))) + posts) + (unless (= page-number 1) + (insert (format "[[./%s][Newer posts]]\n\n" + (ennum-index-filename (file-name-nondirectory filename-prefix) + tongue nil (1- page-number))))) + (unless (= page-number number-of-pages) + (insert (format "[[./%s][Older posts]]\n" + (ennum-index-filename (file-name-nondirectory filename-prefix) + tongue nil (1+ page-number))))) + (org-export-to-file 'html output-file))) + (seq-partition posts posts-per-page) + page-numbers + output-files)) + (copy-file (first output-files) home-page))))) + +(defun ennum--absolute-uri (path) + (format "%s://%s/%s" + (ennum-setting :blog-scheme) + (ennum-setting :blog-domain) + path)) + +(defun ennum--atom-date (date) + (format-time-string "%Y-%m-%dT%H:%M:%SZ" date)) + +(defun ennum-publish-feed (feed-file title rights posts) + (ennum-make-operation + :inputs (seq-map 'ennum-post-filename posts) + :outputs (list feed-file) + :publish + (lambda (output-file) + (with-temp-file output-file + (insert + (xmlgen + `(feed :xmlns "http://www.w3.org/2005/Atom" + (id ,(ennum--absolute-uri "")) + (title ,title) + (updated ,(ennum--atom-date (ennum-post-date (first posts)))) + (link :rel "self" :href ,(ennum--absolute-uri feed-file)) + (generator + ,(format "Emacs %d.%d Org-mode %s ennum %s" + emacs-major-version emacs-minor-version (org-version) ennum-version)) + (rights ,rights) + ,@(seq-map 'ennum--feed-entry posts)))))))) + +(defun ennum--feed-entry (post) + (let ((link (ennum--absolute-uri (ennum--org-output-filename + (ennum-post-filename post))))) + `(entry (id ,link) + (title :xml:lang ,(ennum-post-language post) ,(ennum-post-title post)) + (updated ,(ennum--atom-date (ennum-post-date post))) + ,@(when org-export-with-author + `((author + (name ,(ennum-post-author post)) + (email ,user-mail-address)))) + (content :type "html" :xml:lang ,(ennum-post-language post) + ,(ennum-with-file-contents (ennum-post-filename post) + (org-export-as 'ennum-html nil nil t))) + (link :rel "alternate" :href ,link) + ,@(seq-map (lambda (tag) `(category :term ,tag)) + (ennum-post-tags post))))) + +(defun ennum-setting (property) + (pcase property + ((or :blog-domain :blog-license :blog-title + :images-directory :output-directory :posts-directory + :static-directory :tag-directory :video-directory + :working-directory) + (or (plist-get ennum-blog property) + (user-error "Property %s not defined" property))) + ((or :atom-feed-number-of-posts :atom-feed-file + :blog-scheme :default-image-width + :image-link-width :index-posts-per-page + :locale-alist :other-files-directory + :tag-directory :thumbnail-image-width) + (plist-get (org-combine-plists + (list :atom-feed-number-of-posts 12 + :atom-feed-file "blog.atom" + :blog-scheme "https" + :default-image-width 640 + :image-link-width 1024 + :index-posts-per-page 12 + :locale-alist '(("en" . "C")) + :tag-directory "tag" + :thumbnail-image-width 320) + ennum-blog) + property)) + (_ (error "Unknown property %s" property)))) + +(defun ennum-image-output-filename (image width) + (format "%s-%spx.%s" + (file-name-sans-extension image) + width (file-name-extension image))) + +(defun ennum--expand-relative (name directory) + (concat (file-name-as-directory directory) name)) + +(defun ennum-publish-image (widths image) + (ennum-make-operation + :inputs (list image) + :outputs (seq-map (apply-partially 'ennum-image-output-filename image) + widths) + :publish + (lambda (&rest output-files) + (seq-mapn (lambda (output-file width) + (ennum-image-optimize-image + (ennum-image-resize-image image output-file width))) + output-files widths)))) + +(defun ennum-publish-copy (file) + (ennum-make-operation + :inputs (list file) + :outputs (list file) + :publish (apply-partially 'ennum-copy file))) + +(defun newest-file (files) + (pcase files + (`(,head . ,tail) + (seq-reduce (lambda (file1 file2) + (if (file-newer-than-file-p file1 file2) + file1 file2)) + tail head)))) + +(defun ennum-mkdir-p (directory) + (make-directory directory t)) + +(defun ennum-copy (source destination) + "Copy file or directory from SOURCE to DESTINATION. Overwrite +if DESTINATION already exists." + (if (file-directory-p source) + (copy-directory source destination) + (make-directory (file-name-directory destination) t) + (copy-file source destination t))) + +(defun ennum--filter-map (function sequence) + (seq-filter 'identity (seq-map function sequence))) + +;; TODO: What if a file was removed from the inputs? Detect that +;; change as well. + +;; Two separate problems +;; - tracking of list of inputs +;; - depending on a function of inputs, or equivalently intermediate files + +;; Solve both problems with an "ennum store" +(defun ennum--do-operation (temporary-directory operation) + ;; TODO: Check all outputs were created correctly. + (let* ((expand (lambda (directory file) + (expand-file-name file directory))) + (inputs (ennum-operation-inputs operation)) + (outputs (ennum-operation-outputs operation)) + (absolute-outputs + (seq-map (apply-partially expand temporary-directory) + outputs)) + (previous-outputs + (seq-map (apply-partially expand (ennum-setting :output-directory)) + outputs))) + (cond + ((and (seq-every-p 'file-exists-p previous-outputs) + (file-newer-than-file-p (newest-file previous-outputs) + (newest-file inputs))) + (message "Skipping publishing %s to %s" inputs outputs) + (seq-mapn 'ennum-copy previous-outputs absolute-outputs)) + (t (message "Publishing %s to %s" inputs outputs) + (seq-do 'ennum-mkdir-p + (seq-uniq + (seq-map 'file-name-directory absolute-outputs))) + (apply (ennum-operation-publish operation) absolute-outputs))))) + +(defun ennum-publish-static-file (file) + (ennum-make-operation + :inputs (list file) + :outputs (list file) + :publish (apply-partially 'ennum-copy file))) + +(defun ennum-publish-link (link) + (pcase link + (`("image" . ,path) + (list + (ennum-publish-image + (list (ennum-setting :default-image-width) + (ennum-setting :image-link-width)) + (ennum--expand-relative path (ennum-setting :images-directory))))) + (`("static" . ,path) + (list + (ennum-publish-copy (ennum--expand-relative path (ennum-setting :static-directory))))) + (`("video" . ,path) + (list + (ennum-publish-copy (ennum--expand-relative path (ennum-setting :video-directory))) + (ennum-publish-copy (ennum--expand-relative (ennum-video-poster path) + (ennum-setting :images-directory))))))) + +(defmacro ennum-with-current-directory (directory &rest body) + "Change to DIRECTORY, evaluate BODY and restore the current +working directory. The value returned is the value of the last +form in BODY." + (declare (indent defun)) + (let ((current-directory-symbol (make-symbol "current-directory"))) + `(let ((,current-directory-symbol default-directory)) + (unwind-protect (progn (cd ,directory) ,@body) + (cd ,current-directory-symbol))))) + +(defmacro ennum-with-temporary-directory (temporary-directory &rest body) + "Create temporary directory, evaluate BODY with the absolute +path of that directory assigned to TEMPORARY-DIRECTORY and +finally delete the temporary directory. The value returned is the +value of the last form in BODY." + (declare (indent defun)) + `(let ((,temporary-directory (make-temp-file "ennum" t))) + (chmod ,temporary-directory #o755) + (unwind-protect + (progn ,@body) + (delete-directory ,temporary-directory t)))) + +(defun ennum-many-to-many-group-by (function sequence) + "Apply FUNCTION to each element of SEQUENCE. +Separate the elements of SEQUENCE into an alist using the results +as keys. Keys are compared using `equal'." + (seq-reduce + (lambda (result element) + (seq-do + (lambda (key) + (map-put result key + (cons element (map-elt result key nil 'equal)) + 'equal)) + (funcall function element)) + result) + (seq-reverse sequence) + nil)) + +(defun ennum-publish () + (interactive) + (let ((make-backup-files nil) + (blog-title (ennum-setting :blog-title)) + (posts-per-page (ennum-setting :index-posts-per-page))) + (ennum-with-current-directory (ennum-setting :working-directory) + (ennum-with-temporary-directory temporary-directory + (seq-do + (apply-partially 'ennum--do-operation temporary-directory) + (append + (let ((posts (ennum-posts (ennum-setting :posts-directory)))) + (append + ;; Publish posts + (seq-mapcat (pcase-lambda (`(,translation-group . ,posts)) + (ennum-publish-post posts)) + (seq-group-by 'ennum-post-translation-group posts)) + ;; Publish feed + (list (ennum-publish-feed (ennum-setting :atom-feed-file) + blog-title + (ennum-setting :blog-license) + (seq-take posts (ennum-setting :atom-feed-number-of-posts)))) + ;; Publish indices + (seq-map + (pcase-lambda (`(,tongue . ,posts)) + (ennum-publish-index "index" blog-title posts-per-page posts)) + (seq-group-by 'ennum-post-language posts)) + (seq-mapcat + (pcase-lambda (`(,tag . ,posts)) + (seq-map + (pcase-lambda (`(,tongue . ,posts)) + (ennum-publish-index + (ennum--expand-relative tag (ennum-setting :tag-directory)) + tag posts-per-page posts)) + (seq-group-by 'ennum-post-language posts))) + (ennum-many-to-many-group-by 'ennum-post-tags posts)) + ;; Publish thumbnails + (seq-map + (apply-partially 'ennum-publish-image (list (ennum-setting :thumbnail-image-width))) + (seq-map (lambda (image) + (ennum--expand-relative image (ennum-setting :images-directory))) + (seq-uniq (ennum--filter-map 'ennum-post-thumbnail posts)))))) + ;; Publish other files + (when-let ((other-files-directory (ennum-setting :other-files-directory))) + (seq-map (apply-partially 'ennum-publish-generic other-files-directory) + (seq-map (apply-partially 'string-remove-prefix + (file-name-as-directory (expand-file-name default-directory))) + (directory-files-recursively other-files-directory ".")))))) + ;; Replace old output directory + (let ((output (ennum-setting :output-directory))) + (delete-directory output t) + (rename-file temporary-directory output t)))))) + +;;; Server +;;; +;;; Test HTTP server to serve the blog locally + +;; TODO: Why can't simple-httpd itself handle the unhexing? +(defun ennum-server-start () + (interactive) + (setq httpd-root (expand-file-name (ennum-setting :output-directory) + (ennum-setting :working-directory))) + (defun httpd/ (proc uri-path query request) + (let* ((uri-path (httpd-unhex uri-path)) + (file-path (httpd-gen-path uri-path))) + (cond + ;; If a HTML file other than index.html was requested, reject + ;; that request. + ((and (not (string= (file-name-nondirectory file-path) "index.html")) + (string= (file-name-extension file-path) "html")) + (httpd-error proc 404)) + ;; If the requested file was found, serve it. + ((= (httpd-status file-path) 200) + (httpd-serve-root proc httpd-root uri-path request)) + ;; Perhaps, this is a post or other HTML file that is being + ;; requested. Try serving a file with a .html extension + ;; appended. + (t (httpd-serve-root proc httpd-root (concat uri-path ".html") request))))) + (httpd-start) + (message "Ennum web server listening at http://localhost:%d" httpd-port)) + +(defun ennum-server-stop () + (interactive) + (httpd-stop) + (message "Ennum web server stopped")) + +(provide 'ennum) -- cgit v1.2.3