From 24cba52bfe4e92d2e7082f28d9ae7fe788ef2701 Mon Sep 17 00:00:00 2001 From: Arun Isaac Date: Tue, 20 Aug 2019 23:17:35 +0530 Subject: Initial commit --- ennu-html.el | 86 +++++++++++++ ennu-image.el | 55 ++++++++ ennu.el | 406 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 547 insertions(+) create mode 100644 ennu-html.el create mode 100644 ennu-image.el create mode 100644 ennu.el diff --git a/ennu-html.el b/ennu-html.el new file mode 100644 index 0000000..d2c4b76 --- /dev/null +++ b/ennu-html.el @@ -0,0 +1,86 @@ +;; -*- lexical-binding: t -*- + +(require 'ox) +(require 'subr-x) +(require 'xmlgen) + +(defun expand-file-name* (name default-directory) + (expand-file-name name (concat "/" default-directory))) + +(org-export-define-derived-backend 'ennu-html 'html + :options-alist + '((:html-inline-image-rules + nil nil '(("image" . "\\.\\(jpeg\\|jpg\\|png\\|gif\\|svg\\)\\'")) t) + (:summary "SUMMARY" nil nil t) + (:thumbnail "THUMBNAIL" nil nil t))) + +;; 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) + (let ((post (concat (expand-file-name path (ennu-setting :posts-directory)) + ".org"))) + (xmlgen `(a :href ,(expand-file-name* + path (ennu-setting :posts-directory)) + ,(or desc (plist-get (ennu-post-metadata post) :title)))))) + +(org-link-set-parameters + "post" :export 'ennu-export-post) + +(defun ennu-export-image (path desc backend) + (let ((img `(img :src ,(expand-file-name* + (ennu-image-output-filename + path (ennu-setting :default-image-width)) + (ennu-setting :images-directory))))) + (xmlgen (if (ennu-setting :image-link-width) + `(a :href ,(expand-file-name* + (ennu-image-output-filename + path (ennu-setting :image-link-width)) + (ennu-setting :images-directory)) + ,img) + img)))) + +(org-link-set-parameters + "image" :export 'ennu-export-image) + +(defun ennu-export-thumbnail (path desc backend) + (xmlgen + `(img :src ,(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) + +(defun ennu-export-video (path desc backend) + (let ((video-directory (ennu-setting :video-directory))) + (xmlgen + `(video :src ,(expand-file-name* path video-directory) + :poster ,(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) + (xmlgen + `(a :href ,(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) + (xmlgen + `(a :href ,(expand-file-name* (concat tag ".html") + (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 new file mode 100644 index 0000000..34c3e7e --- /dev/null +++ b/ennu-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 '(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 new file mode 100644 index 0000000..2e54938 --- /dev/null +++ b/ennu.el @@ -0,0 +1,406 @@ +;; -*- lexical-binding: t -*- + +(require 'ennu-html) +(require 'ennu-image) +(require 'ox) +(require 'seq) +(require 'cl) +(require 'memoize) + +(defmacro ennu--with-file-contents (file &rest 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 (post) + ;; TODO: Tangle post + `((,post) + (,(ennu--org-output-filename post)) + ,(lambda (output-file) + (ennu--with-file-contents post + (org-export-to-file 'ennu-html output-file)) + ;; (when-let (tangle-dir (or (plist-get posts-plist :valai-tangle-directory) + ;; valai-tangle-directory)) + ;; (dolist (tangled-file + ;; (org-babel-tangle-file post-path)) + ;; (when (member (file-name-extension tangled-file) '("sh" "py")) + ;; (chmod tangled-file (string-to-number "755" 8))) + ;; (make-directory tangle-dir t) + ;; (rename-file tangled-file + ;; (expand-file-name (file-name-nondirectory tangled-file) + ;; tangle-dir) + ;; t))) + ))) + +(defun ennu-publish-page (pages-directory page) + `((,page) + (,(ennu--org-output-filename + (string-remove-prefix + (file-name-as-directory pages-directory) + page))) + ,(lambda (output-file) + (ennu--with-file-contents page + (org-export-to-file 'html 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-post-thumbnail (post) + (or (plist-get (ennu-post-metadata post) :thumbnail) + (seq-some (lambda (link) + (pcase link + (`("image" . ,path) path) + (`("video" . ,path) (ennu-video-poster path)))) + (ennu-post-links post)))) + +(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))))) + +(defun ennu-index-filename (filename-prefix tongue extension &optional page-number) + (ennu-add-tongue-suffix + (if page-number + (format "%s-%s.%s" + (file-name-nondirectory filename-prefix) + page-number + extension) + (format "%s.%s" (filename-nondirectory filename-prefix) extension)) + tongue)) + +(defun ennu-publish-index (filename-prefix tongue title subtitle posts-per-page posts) + (let* ((number-of-pages (ceiling (length posts) posts-per-page)) + (page-numbers (number-sequence 1 number-of-pages))) + `(,posts + ,(cons (ennu-add-tongue-suffix (format "%s.html" filename-prefix) tongue) + (seq-map (apply-partially 'ennu-index-filename filename-prefix tongue "html") + page-numbers)) + ,(lambda (home-page &rest output-files) + (seq-mapn + (lambda (posts page-number output-file) + (with-temp-buffer + (insert (format "#+TITLE: %s\n" title subtitle)) + (insert "#+OPTIONS: num:nil toc:nil\n\n") + (when subtitle + (insert (format "%s\n\n" subtitle))) + (seq-do (lambda (post) + (let ((metadata (ennu-post-metadata post))) + (insert (format "* [[post:%s]]\n" (file-name-base post))) + (insert (format-time-string + "/%b %e, %Y/\n\n" + (plist-get metadata :date))) + (when-let ((thumbnail (ennu-post-thumbnail post))) + (insert (format "[[thumbnail:%s]]\n\n" thumbnail))) + (when-let ((summary (plist-get metadata :summary))) + (insert summary) + (insert "\n\n")) + (when-let ((tags (plist-get metadata :filetags))) + (insert "Tags: ") + (insert + (string-join (seq-map (apply-partially 'format "[[tag:%s]]") + tags) + ", ")) + (insert "\n\n")))) + posts) + (unless (= page-number 1) + (insert (format "[[./%s][Newer posts]]\n" + (ennu-index-filename filename-prefix tongue "org" (1- page-number))))) + (unless (= page-number number-of-pages) + (insert (format "[[./%s][Older posts]]\n" + (ennu-index-filename filename-prefix tongue "org" (1+ page-number))))) + (org-export-to-file 'ennu-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 subtitle rights posts) + `(,posts + (,feed-file) + ,(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 (plist-get (ennu-post-metadata (first posts)) :date))) + (link :rel "self" :href ,(ennu--absolute-uri feed-file)) + (generator + ;; TODO: Change this generator + ,(format "Emacs %d.%d Org-mode %s ennu" + emacs-major-version emacs-minor-version (org-version))) + (rights ,rights) + ,@(when subtitle + `((subtitle ,subtitle))) + ,@(seq-map 'ennu--feed-entry posts)))))))) + +(defun ennu--feed-entry (post) + (let* ((metadata (ennu-post-metadata post)) + (lang (plist-get metadata :lang)) + (link (ennu--absolute-uri (ennu--org-output-filename post)))) + `(entry (id ,link) + (title :xml:lang ,lang ,(plist-get metadata :title)) + (updated ,(ennu--atom-date (plist-get metadata :date))) + (author + (name ,(plist-get metadata :author)) + (email ,user-mail-address)) + (content :type "html" :xml:lang ,lang + ,(ennu--with-file-contents post + (org-export-as 'ennu-html nil nil t))) + (link :rel "alternate" :href ,link) + ,@(seq-map (lambda (tag) `(category :term ,tag)) + (plist-get metadata :filetags))))) + +(defun ennu-setting (property) + (pcase property + (:blog-scheme + (or (plist-get ennu-blog :blog-scheme) + "https")) + (:atom-feed-file + (or (plist-get ennu-blog :atom-feed-file) + "blog.atom")) + (:index-posts-per-page + (or (plist-get ennu-blog :index-posts-per-page) + 12)) + (:atom-feed-number-of-posts + (or (plist-get ennu-blog :atom-feed-number-of-posts) + 12)) + (:thumbnail-image-width + (or (plist-get ennu-blog :thumbnail-image-width) + 320)) + (:default-image-width + (or (plist-get ennu-blog :default-image-width) + 640)) + (:image-link-width + (or (plist-get ennu-blog :image-link-width) + 1024)) + ((or :blog-domain :blog-license :blog-title + :images-directory :output-directory :posts-directory + :static-directory :tag-directory :video-directory) + (or (plist-get ennu-blog property) + (user-error "Property %s not defined" property))) + ((or :blog-subtitle :pages-directory :unattached-static-files) + (plist-get 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) + `((,image) + ,(seq-map (apply-partially 'ennu-image-output-filename image) + widths) + ,(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) + `((,file) (,file) ,(apply-partially 'copy-file file))) + +(defun ennu-post-links (post) + (ennu--with-file-contents post + (org-element-map (org-element-parse-buffer) '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)))))))))) + +(defun plist-put* (plist &rest key-value-pairs) + (pcase key-value-pairs + (`(,key ,value) + (plist-put plist key value)) + (`(,key ,value . ,tail) + (apply 'plist-put* (plist-put plist key value) tail)))) + +(defun ennu-post-metadata (post) + (ennu--post-metadata-memoized + post (file-attribute-modification-time + (file-attributes post)))) + +(defmemoize ennu--post-metadata-memoized (post last-modified) + (ennu--with-file-contents post + (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) + (plist-put* + metadata + :title (funcall export (first (plist-get metadata :title))) + :date (org-timestamp-to-time (first (plist-get metadata :date))) + :author (funcall export (first (plist-get metadata :author))))))) + +(defvar ennu-mandatory-metadata + (list :title :date)) + +(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." + (if (file-directory-p source) + (copy-directory source destination) + (make-directory (file-name-directory destination) t) + (copy-file source destination))) + +(defun ennu--do-operation (temporary-directory operation) + (let ((expand (lambda (directory file) + (expand-file-name file directory)))) + (pcase operation + (`(,input-files ,output-files ,publish) + (let ((absolute-output-files + (seq-map (apply-partially expand temporary-directory) + output-files)) + (previous-output-files + (seq-map (apply-partially expand (ennu-setting :output-directory)) + output-files))) + (cond + ((and (seq-every-p 'file-exists-p previous-output-files) + (file-newer-than-file-p (newest-file previous-output-files) + (newest-file input-files))) + (message "Skipping publishing %s to %s" input-files output-files) + (seq-mapn 'ennu-copy previous-output-files absolute-output-files)) + (t (message "Publishing %s to %s" input-files output-files) + (seq-do 'ennu-mkdir-p + (seq-uniq + (seq-map 'file-name-directory absolute-output-files))) + (apply publish absolute-output-files)))))))) + +(defun ennu--later-post (post1 post2) + (time-less-p (plist-get (ennu-post-metadata post2) :date) + (plist-get (ennu-post-metadata post1) :date))) + +(defun ennu-publish-static-file (file) + `((,file) (,file) ,(apply-partially 'copy-file file))) + +(defun ennu-posts (posts-directory) + (sort (file-expand-wildcards + (concat (file-name-as-directory + (ennu-setting :posts-directory)) + "*.org")) + 'ennu--later-post)) + +(defun ennu-publish-link (link) + (pcase link + (`("image" . ,path) + (ennu-publish-image + (list (ennu-setting :default-image-width) + (ennu-setting :image-link-width)) + (ennu--expand-relative path (ennu-setting :images-directory)))) + (`("static" . ,path) + (ennu-publish-copy (ennu--expand-relative path (ennu-setting :static-directory)))) + (`("video" . ,path) + (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)))))) + +(defun ennu-post-tags (post) + (plist-get (ennu-post-metadata post) :filetags)) + +(defun ennu-post-tongue (post) + (plist-get (ennu-post-metadata post) :language)) + +(defun ennu-publish () + (let ((make-backup-files nil) + (blog-title (ennu-setting :blog-title)) + (blog-subtitle (ennu-setting :blog-subtitle)) + (posts-per-page (ennu-setting :index-posts-per-page))) + (let ((temporary-directory (make-temp-file "ennu" t))) + (unwind-protect + (progn + (seq-do + (apply-partially 'ennu--do-operation temporary-directory) + (append + (let* ((posts (ennu-posts (ennu-setting :posts-directory))) + (tags (seq-uniq (seq-mapcat 'ennu-post-tags posts))) + (tongues (seq-uniq (seq-map 'ennu-post-tongue posts)))) + (append + ;; Publish posts + (seq-map 'ennu-publish-post posts) + ;; Publish feed + (list (ennu-publish-feed (ennu-setting :atom-feed-file) + blog-title blog-subtitle + (ennu-setting :blog-license) + (seq-take posts (ennu-setting :atom-feed-number-of-posts)))) + ;; Publish indices + (seq-map + (lambda (tongue) + (ennu-publish-index + "index" tongue blog-title blog-subtitle posts-per-page + (seq-filter (lambda (post) + (string= tongue (ennu-post-tongue post))) + posts))) + tongues) + (seq-map + (lambda (tag) + (let ((posts (seq-filter (lambda (post) + (member tag (ennu-post-tags post))) + posts))) + (ennu-publish-index + (ennu--expand-relative tag (ennu-setting :tag-directory)) + (ennu-post-tongue (first posts)) tag "" posts-per-page posts))) + tags) + ;; Publish links + (seq-map 'ennu-publish-link + (seq-uniq (seq-mapcat 'ennu-post-links 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 (seq-filter 'identity (seq-map 'ennu-post-thumbnail posts))))))) + ;; Publish pages + (when-let ((pages-directory (ennu-setting :pages-directory))) + (seq-map (apply-partially 'ennu-publish-page pages-directory) + (seq-map (apply-partially 'string-remove-prefix + (file-name-as-directory (expand-file-name default-directory))) + (directory-files-recursively pages-directory "\\.org$")))) + ;; Publish unattached static files + (seq-map 'ennu-publish-static-file + (ennu-setting :unattached-static-files)))) + ;; Replace old output directory + (let ((output (ennu-setting :output-directory))) + (delete-directory output t) + (rename-file temporary-directory output t))) + (delete-directory temporary-directory t))))) + +(provide 'ennu) -- cgit v1.2.3