Org-mode has a flexible export framework which can be leveraged to generate Html content suitable for publishing with Jekyll, a static site generator. This post describes the elisp package which is used to maintain my blog at MonadicT.github.io.
Blog entries are written as a regular .org
file. A post should have
a #+title
, #+summary
, #+publish_date
, #+tags
and
#+export_file_name
for proper formatting and structure. The
export_file_name
header is used by Org-mode to create the generated
Html file.
The entire site with Home, About, Tags, RSS and all the posts can be
regenerated by Meta blog-gen-publish
, also bound to C-c C-g
. An
individual post can be quickly examined by invoking Org export menu
(C-c C-e
) which shows the options for processing the buffer. C-c
C-e
C-f will generate a HTML file.
If there is no #+publish-date
header, the post will be skipped when
the site is regenerated.
A new blog entry with all the required headers can be created by
executing blog-gen-new-post
.
Not many! yatl
is my Elisp package for generating Html fragments
from s-expressions.
(require 'cl) (require 'subr-x) (require 's) (require 'yatl)
A quick little macro to write assertions.
(defmacro assert-equal (actual expected) `(when (not (equal ,actual ,expected)) (error "Actual: %s expected: %s" (format "%s" ',actual) (format "%s" ',expected))))
Parses key-value string of form @key=val@key=val.
(defun blog-gen-parse-kvp(s) (let (res) (mapconcat (lambda (kv-str) (let* ((kv (split-string kv-str "=")) (key (car kv)) (val (cadr kv)) (val (if (string-match "^\".*\"$" val) val (format "\"%s\"" val)))) (concat key "=" val))) (split-string s "@" t) " "))) (assert-equal (blog-gen-parse-kvp "") "") (assert-equal (blog-gen-parse-kvp "@") "") (assert-equal (blog-gen-parse-kvp "@a=b") "a=\"b\"") (assert-equal (blog-gen-parse-kvp "@a=b@c=d") "a=\"b\" c=\"d\"")
(defmacro lisp (&rest forms) `(let ((res ,@forms)) res)) (assert-equal (lisp 1) 1) (assert-equal (lisp (+ 1 2 3)) 6) (assert-equal (lisp (concat "a" "n")) "an") (assert-equal (lisp (list 1 2 3)) '(1 2 3))
Parses element name and returns the list of element name, id, class
and attributes. Multiple class names but id must be unique. Id is
introduced by ?
, class name with .
and attribute with @
.
;; Exanple names: div, div?id, div.cls1.clas2, div!k=v!k=v (defun blog-gen-parse-elem-name(s) (let ((nm-id-cls (list '() '() '() '())) attrs idx buf) (setq idx 0) (mapcar (lambda (c) (cond ((eq c ?.) (progn (setq idx 2) (push c (nth idx nm-id-cls)))) ((eq c ??) (progn (if (null (cadr nm-id-cls)) (setq idx 1) (error "ID specified again!")))) ((eq c ?@) (progn (setq idx 3) (push c (nth idx nm-id-cls)))) ((eq idx -1) (error "Expect one of \".,+,#'")) (t (push c (nth idx nm-id-cls))))) s) (list (concat (reverse (car nm-id-cls))) (concat (reverse (cadr nm-id-cls))) (concat (reverse (caddr nm-id-cls))) (blog-gen-parse-kvp (concat (reverse (nth 3 nm-id-cls))))))) (assert-equal (blog-gen-parse-elem-name "div") '("div" "" "" "")) (assert-equal (blog-gen-parse-elem-name "div?id") '("div" "id" "" "")) (assert-equal (blog-gen-parse-elem-name "div?id.c1.c2") '("div" "id" ".c1.c2" "")) (assert-equal (blog-gen-parse-elem-name "div?id.c1.c2@foo=bar@baz=qux") '("div" "id" ".c1.c2" "foo=\"bar\" baz=\"qux\""))
(require 'ox-html) ;;; Variables and options (defgroup org-export-blog nil "Options specific to RSS export back-end." :tag "Org Blog" :group 'org-export :version "24.4" :package-version '(Org . "9.0")) (defcustom blog-gen-publish-url "https://MonadicT.github.io" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-title "MonadicT" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-tagline "I see dead objects!" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-author "Praki Prakash" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-copyright-message "Copyright © 2014-%s, Praki Prakash" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-style-file "blog-style.css" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-banner-file "banner.org" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-footer-file "footer.org" "???" :group 'org-export-blog :type 'string)
This function is called from Org-export machinery. The main content of the article is wrapped up as a full blog page.
(defun blog-gen-inner-template (contents info) (yatl-html-frag (body (div@style=display:flex (div@style=flex:2 "") (div@style=flex:6 (div (blog-gen-top-matter) contents)) (div@style=flex:2 "") (div.blog-footer (format blog-gen-copyright-message (format-time-string "%Y")))))))
(defun blog-gen-twitter-link() "<a target=\"_new\" href=\"https://twitter.com/MonadicT\"> <span style={background-color: white; height:48px;width:48px;border-radius:24px}></span> <img height=\"48px\" width=\"48px\" title=\"Visit my Twitter page\" src=\"/images/twitter.png\"/></a>")
(defun blog-gen-github-link() "<a id=\"github-link\" target=\"_new\" href=\"https://github.com/MonadicT\"><img id=\"github-logo\" src=\"/images/github-logo.png\"/></a>")
(defun blog-gen-home-link() "<a href=\"/index.html\">Home</a>")
(defun blog-gen-articles-link() "<a href=\"/articles.html\">Articles</a>")
(defun blog-gen-about-link() "<a href=\"/about.html\">About</a>")
(defun blog-gen-tags-link() "<a href=\"/tags.html\">Tags</a>")
(defun blog-gen-site-links() (concat "<div id=\"site-links\">" (blog-gen-home-link) (blog-gen-articles-link) (blog-gen-tags-link) (blog-gen-about-link) "</div>"))
Generates the header and the banner. Here is one limitation of
yatl-compile-string
which can generate a single element with dynamic
content but not nested elements as needed here.
(defun blog-gen-top-matter() (yatl-html-frag (div.blog-nav (div?blog-title blog-gen-title (div?tagline blog-gen-tagline)) (blog-gen-search-form)) (div.blog-nav (blog-gen-site-links) (blog-gen-social-media-icons)) (div@style=border:1px "")))
(defun blog-gen-search-form() "<div>Search <form action=\"http://www.google.com/search\" id=\"searchform\" method=\"get\"><div><input class=\"box\" id=\"s\" name=\"q\" type=\"text\" /> <input name=\"sitesearch\" type=\"hidden\" value=\"http://MonadicT.github.io\" /> </div></form></div>")
(defun blog-gen-social-media-icons() (concat "<div id=\"social-media-icons\">" (blog-gen-twitter-link) (blog-gen-github-link) "</div>"))
;;; Define backend (org-export-define-derived-backend 'blog 'html :menu-entry '(?b "Export to Blog" ((?b "As Blog buffer" (lambda (a s v b) (blog-gen-export-as-blog a s v))) (?f "As Blog file" (lambda (a s v b) (blog-gen-export-to-blog a s v))) (?o "As Blog file and open" (lambda (a s v b) (if a (blog-gen-export-to-blog t s v) (org-open-file (blog-gen-export-to-blog nil s v))))))) :options-alist '((:description "DESCRIPTION" nil nil newline) (:keywords "KEYWORDS" nil nil space) (:with-toc nil nil nil) ;; Never include HTML's toc ) :filters-alist '((:filter-final-output . blog-gen-final-function)) :translate-alist '((comment . (lambda (&rest args) "")) (comment-block . (lambda (&rest args) "")) (timestamp . (lambda (&rest args) "")) (inner-template . blog-gen-inner-template) (template . blog-gen-template))) ;;; Export functions ;;;###autoload (defun blog-gen-export-as-blog (&optional async subtreep visible-only) "Export current buffer to a blog buffer. Export is done in a buffer named \"*Org Blog Export*\", which will be displayed when `org-export-show-temporary-export-buffer' is non-nil." (interactive) (let ((file (buffer-file-name (buffer-base-buffer))))) (org-export-to-buffer 'blog "*Org Blog Export*" async subtreep visible-only nil nil (lambda () (text-mode)))) ;;;###autoload (defun blog-gen-export-to-blog (&optional async subtreep visible-only) "Export current buffer to a Blog file. Return output file's name." (interactive) (let ((file (buffer-file-name (buffer-base-buffer))))) (let ((outfile (org-export-output-file-name (concat "." "html") subtreep))) (org-export-to-file 'blog outfile async subtreep visible-only))) ;;;###autoload (defun blog-gen-publish-to-blog (plist filename pub-dir) "Publish an org file to Blog. FILENAME is the filename of the Org file to be published. PLIST is the property list for the given project. PUB-DIR is the publishing directory. Return output file name." (let ((bf (get-file-buffer filename))) (if bf (with-current-buffer bf (write-file filename)) (find-file filename) (write-file filename) (kill-buffer))) (org-publish-org-too 'log filename (concat "." "html") plist pub-dir)) ;;; Main transcoding functions (defun blog-gen-template (contents info) "Return complete document string after BLOG conversion. CONTENTS is the transcoded contents string. INFO is a plist used as a communication channel." (yatl-html5 (head (yatl-compile-string "meta@charset=\"%s\">" (symbol-name org-html-coding-system)) (title blog-gen-title) (yatl-compile-string "base@href=\"%s\"" (blog-gen-base-url)) (yatl-compile-string "meta@name=generator@content=\"%s\">" (emacs-version)) (yatl-compile-string "meta@name=author@content=\"%s\"" blog-gen-author) "<link href=\"https://fonts.googleapis.com/css?family=Source+Code+Pro|EB+Garamond:800|Roboto:100,300,400,400i,700\" rel=\"stylesheet\">" (link@href=\"blog-style.css\"@rel=\"stylesheet\")) contents)) ;;; Filters (defun blog-gen-final-function (contents backend info) "Prettify the Blog output." (with-temp-buffer (xml-mode) (insert contents) ;;(indent-region (point-min) (point-max)) (buffer-substring-no-properties (point-min) (point-max)))) ;;; Miscellaneous (provide 'ox-blog) ;;; ox-blog.el ends here
(require 'ox-html) (defun my-html-body-filter(text backend info) text) (add-to-list 'org-export-filter-body-functions 'my-html-body-filter)
CSS Styles used in this blog are managed using interpolated strings. The code below is an association list which is used to build CSS string later.
Font sizes are specified in percent units which makes the content scalable.
CSS color definitions.
#616161
#9E9E9E
#d3d3d3
#545454
Previous color was #ff5722
.
#51c0ae
#545454
#757575
#FF5722
#51c0ae
Source code is rendered against this background color for readbility.
#f2f2f2
Style definitions for general html elements.
/Setting body.height to 100% paints background to visible portion of blog content and leaving it empty results in left and right borders at zero height. The only reasonable fix is to be able to specify heights for all layout divs to be in resolution independent units/
* { font-family: "Roboto"; } html { clear: both; Height: 100%; width: 100% } body { margin: 0; padding: 0; width: 100%; display: flex; flex-direction: column; color: #545454; background-color: #fffffd; line-height: 1.35; padding: 1em; } a { color: #51c0ae; text-decoration: none} a:visited { color: #51c0ae; text-decoration: none} a:hover { color: #51c0ae; opacity: 0.5; }
h2 { color: #51c0ae; } /* */ #blog-title { font-family: 'Cormorant Garamond'; font-size: 400%; font-weight: bolder; color: #51c0ae; } #tagline { font-size: 20%; font-weight: lighter; padding-bottom: 1em; } .blog-nav { display: flex; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 2px solid #999 }
#site-links { } #site-links a { color: #51c0ae; font-size: 100%; text-decoration: none; padding-right: 1em } #social-media-icons { } #github-logo { vertical-align: super }
.post-title { font-size: 150%; font-weight: bold; color: #51c0ae; border-bottom: 2px solid #ddd } .post-summary { font-size: 100%; color: #757575; padding-top: 0.5em; padding-left: 1em; } .post-tags { font-size: 80%; color: #757575; padding-top: 1em; padding-left: 1em; } .pub-date { font-weight: bold; color: #757575; padding-bottom: 2em; } .post-publish-date { font-size: 80%; color: #777; padding-top: 0.5em; padding-left: 1em; padding-bottom: 1em; }
Here we override style information for elements generated by org-mode.
.org-src-container { border-left: 0.2em solid #999; } .src { font-family: 'Source Code Pro'; font-size: 110%; background-color: #f2f2f2; padding: 1em; }
.blog-footer { display: flex; position: fixed; bottom: 0; width: 100%; height: 1em; font-size: 50%; font-weight: bold; padding: 5px; flex-direction: row; justify-content: center; border-top: solid 1px #dfe3ee; color: #51c0ae; /* #3b5998, #dfe3ee, #8b9dc3*/ background-color: #d3d3d3; /*#dfe3ee;*/ } /* , #8b9dc3 */
.org-src-container { border-left: 0.2em solid #999; } .src { font-family: 'Source Code Pro'; font-size: 110%; background-color: #f2f2f2; padding: 1em; } * { font-family: "Roboto"; } html { clear: both; Height: 100%; width: 100% } body { margin: 0; padding: 0; width: 100%; display: flex; flex-direction: column; color: #545454; background-color: #fffffd; line-height: 1.35; padding: 1em; } a { color: #51c0ae; text-decoration: none} a:visited { color: #51c0ae; text-decoration: none} a:hover { color: #51c0ae; opacity: 0.5; } h2 { color: #51c0ae; } /* */ #blog-title { font-family: 'Cormorant Garamond'; font-size: 400%; font-weight: bolder; color: #51c0ae; } #tagline { font-size: 20%; font-weight: lighter; padding-bottom: 1em; } .blog-nav { display: flex; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 2px solid #999 } .post-title { font-size: 150%; font-weight: bold; color: #51c0ae; border-bottom: 2px solid #ddd } .post-summary { font-size: 100%; color: #757575; padding-top: 0.5em; padding-left: 1em; } .post-tags { font-size: 80%; color: #757575; padding-top: 1em; padding-left: 1em; } .pub-date { font-weight: bold; color: #757575; padding-bottom: 2em; } .post-publish-date { font-size: 80%; color: #777; padding-top: 0.5em; padding-left: 1em; padding-bottom: 1em; } .blog-footer { display: flex; position: fixed; bottom: 0; width: 100%; height: 1em; font-size: 50%; font-weight: bold; padding: 5px; flex-direction: row; justify-content: center; border-top: solid 1px #dfe3ee; color: #51c0ae; /* #3b5998, #dfe3ee, #8b9dc3*/ background-color: #d3d3d3; /*#dfe3ee;*/ } /* , #8b9dc3 */ #site-links { } #site-links a { color: #51c0ae; font-size: 100%; text-decoration: none; padding-right: 1em } #social-media-icons { } #github-logo { vertical-align: super }
Returns list of OrgMode keywords from the current document.
(defun blog-gen-get-all-keywords() (org-element-map (org-element-parse-buffer 'element) 'keyword (lambda (kw) (cons (org-element-property :key kw) (org-element-property :value kw)))))
Returns value of key
or default-value
if key
doesn't exist in keywords
.
(defun blog-gen-get-keyword-value(keywords key &optional default-value) (if-let ((kw-value (cdr (assoc-ignore-case key keywords)))) kw-value default-value))
To make any changes to blog generation, this file should be
tangled. Tangling this file generates ~/.emacs.d/lisp/blog-gen.el
and blog-style.css
. blog-gen-publish
bound to C-c C-g
, processes
all org files and generates HTML suitable for publishing as a static
web site on a local server. With a prefix argument, the generated HTML
will have s different base url
suotable for publishing to github.io
.
A local server for serving HTML can be run by python -m http.server
8000
command.
The root directory where the source for blogs is kept.
(defcustom blog-gen-root-dir "~/projects/MonadicT.github.io/" "")
The subdirectory where .org
files are stored.
(defcustom blog-gen-posts-dir "_resources/posts/" "")
This is a Boolean flag set to use base
url for generated html files.
(setq blog-gen-local t)
(defun blog-gen-base-url() (if blog-gen-local "http://localhost:8000/" "https://MonadicT.github.io/"))
Extracts post title, summary and publish-date
from the file. Nil is
returned if publish-date
is not present.
(defun blog-gen-post-details (f) (with-temp-buffer (find-file f) (let* ((kws (blog-gen-get-all-keywords)) (details (make-hash-table :test #'equal)) (export-file-name (blog-gen-get-keyword-value kws "export_file_name" nil)) (href (or export-file-name ""))) (while (string-match "^\\.\\./" href) (setq href (substring href 3))) (puthash "post-file" f details) (puthash "title" (blog-gen-get-keyword-value kws "title" "") details) (puthash "summary" (blog-gen-get-keyword-value kws "summary" "") details) (puthash "publish-date" (blog-gen-get-keyword-value kws "publish-date" nil) details) (puthash "export_file_name" export-file-name details) (puthash "tags" (blog-gen-get-keyword-value kws "tags" "") details) (puthash "target" (blog-gen-get-keyword-value kws "target" "") details) (puthash "href" (concat (blog-gen-base-url) href) details) (unless (string-match "blog-generator.org" f) (kill-buffer)) details)))
Returns list of posts stored in .org
files. .org
files such as
index.org
, about.org
are not returned as posts.
(defun blog-gen-post-files() (let* ((posts-dir (concat blog-gen-root-dir "/" blog-gen-posts-dir)) (org-files (directory-files posts-dir t "[a-ZA-Z0-9_-]*\\.org$")) (org-files (seq-remove (lambda (f) (or (string-match "index.org$" f) (string-match "about.org$" f) (string-match "sitemap.org$" f))) org-files))) (mapcar #'blog-gen-post-details org-files)))
Returns published posts (posts which have export_file_name
keyword).
(defun blog-gen-published-posts (posts) (seq-filter (lambda (p) (gethash "export_file_name" p)) posts))
Orders posts by publish-date
descending.
(defun blog-gen-order-posts(posts) (seq-sort (lambda (a b) (string> (gethash "publish-date" a) (gethash "publish-date" b))) posts))
html_export
blocks(defmacro html-export(&rest content) `(progn (insert "#+BEGIN_EXPORT html\n") (insert ,@content) (insert "\n#+END_EXPORT\n\n")))
Exports all .org
post files to .html
files.
(defun blog-gen-create-posts() (let ((posts (blog-gen-published-posts (blog-gen-post-files)))) (mapcar (lambda (post) (let ((post-file (gethash "post-file" post)) (export-file-name (gethash "export_file_name" post))) (message (concat "exporting" post-file "to" export-file-name)) (when export-file-name (with-temp-buffer (find-file post-file) (org-export-to-file 'blog export-file-name) (kill-buffer))))) posts)))
Generates list of articles.
(defun blog-gen-create-articles() (with-temp-buffer (find-file (concat blog-gen-root-dir "index.org")) (erase-buffer) (insert "#+title: MonadictT\n") (insert "#+options: num:nil html-style:nil\n") (insert "#+HTML_HEAD: <link href=\"https://fonts.googleapis.com/css?family=Cormorant+Garamond|Roboto\" rel=\"stylesheet\">\n") (insert "#+HTML_HEAD_EXTRA: <style>* {font-family: 'Roboto';}</style>\n") (insert "#+HTML_HEAD_EXTRA: <style>pre {font-family: 'Segoe Print';}</style>\n") (insert "* Posts\n") (let ((posts (blog-gen-order-posts (blog-gen-published-posts (blog-gen-post-files))))) (mapcar (lambda (post) (let* ((title (gethash "title" post)) (summary (gethash "summary" post)) (export-file-name (gethash "export_file_name" post)) (export-file-name (let ((href export-file-name)) (while (string-match "^\\.\\./" href) (setq href (substring href 3))) href)) (publish-date (gethash "publish-date" post)) (l (list (make-symbol (format "a@href=\"/%s\"" export-file-name)) title))) (html-export (yatl-html-frag (div.post-title (eval (yatl-compile-fn l))))) (html-export (yatl-html-frag (div.post-summary summary)) "\n\n") (html-export (yatl-html-frag (div.post-publish-date "Published: " publish-date)) "\n"))) posts)) (save-buffer) (let ((out-file (concat blog-gen-root-dir "articles.html"))) (when (file-exists-p out-file) (delete-file out-file)) (org-export-to-file 'blog out-file)) (delete-file (concat blog-gen-root-dir "index.org")))) ;;(blog-gen-create-articles)
Posts have a #+tags
header and tags are separated by ,
. This
function builds a hashtable of tags to a list posts from posts
.
(defun create-tag-post-map(posts) (let ((tag-post-map (make-hash-table :test 'equal))) (mapcar (lambda(post) (let ((title (gethash "title" post)) (tags (mapcar #'s-trim (split-string (gethash "tags" post) "[,]+")))) (mapcar (lambda (tag) (when (not (string-empty-p tag)) (puthash tag (cons post (gethash tag tag-post-map '())) tag-post-map))) tags))) posts) tag-post-map))
Tags should be ordered in lexicographic order. This function returns tags in ascending order.
(defun sorted-keys(tag-post-map) (let ((keys '())) (maphash (lambda (k v) (setq keys (cons k keys))) tag-post-map) (seq-sort (lambda (a b) (string-lessp (upcase a) (upcase b))) keys)))
Once we have the tag-post-map
, we can generate an org-mode
representation of it. The tag is listed as second-level header with a
bullet list of anchors to posts.
(defun blog-gen-create-tags(posts) (let* ((tag-post-map (create-tag-post-map posts)) (tags (sorted-keys tag-post-map))) (with-temp-buffer (find-file "tags.org") (erase-buffer) (insert "#+title: Tags\n") (insert "#+options: num:nil\n") (insert "* Tags\n") (mapcar (lambda (tag) (let ((post-list (gethash tag tag-post-map))) (insert "** " tag "\n") (mapcar (lambda (post) (princ post) (let ((title (gethash "title" post)) (href (gethash "href" post))) (insert "- [[" href "][" title "]]\n"))) post-list))) tags) (write-file "tags.org") (let ((out-file (concat blog-gen-root-dir "tags.html"))) (org-export-to-file 'blog out-file)) (kill-buffer)))) ;; (setq x (blog-gen-post-files)) ;; (blog-gen-create-tags x) ;; (require 'blog-gen)
(defun blog-gen-create-about() (with-temp-buffer (find-file (concat blog-gen-root-dir blog-gen-posts-dir "about.org")) (next-line) ;; When on line 1, org export throws a weird error (org-export-to-file 'blog (concat blog-gen-root-dir "about.html") nil ) (kill-buffer))) (blog-gen-create-about)
For now, Home points to Articles.
(defun blog-gen-create-home() (copy-file (concat blog-gen-root-dir "articles.html") (concat blog-gen-root-dir "index.html") t))
Function to regenerate the full site. This is bound to C-c C-g
.
(defun blog-gen-publish(prod) (interactive "P") (message "Blog generattion started") (if prod (setq blog-gen-local nil) (setq blog-gen-local t)) (message (format "Local Gen %S" blog-gen-local)) (let ((posts (blog-gen-post-files))) (blog-gen-create-posts) (message "Created posts") (blog-gen-create-articles) (message "Created articles page") (blog-gen-create-tags posts) (message "Created tags page") (blog-gen-create-about) (message "Created about page") (blog-gen-create-home) (message "Created home page")) (when prod (when (blog-gen-file-contains-p "localhost") (error "localhost references in file")))) (defun blog-gen-file-contains-p(s) nil) (global-set-key (kbd "C-c C-g") #'blog-gen-publish)
A template for creating a new blog post.
(defun blog-gen-new-post() (interactive) (insert "#+title: TBD #+summary: TBD #+publish-date: 2018-01-31 #+export_file_name: ../../yyyy/TBD #+tags: TBD #+option: num:nil"))
All the functions described above are tangled into
~/.emacs.d/lisp/blog-gen.el. A require
in .emacs
will make this
available for use.
;;; blog-gen.el --- Static site generator in emacs and org-mode ;; Copyright (C) 2017-2018 Praki Prakash ;; Author: Praki Prakash ;; Maintainer: Praki Prakash ;; Created: 2017-12-31 ;; Keywords: languages ;; Homepage: https://MonadicT.gihub.io ;; This file is not part of GNU Emacs.
;;; blog-gen.el --- Static site generator in emacs and org-mode ;; Copyright (C) 2017-2018 Praki Prakash ;; Author: Praki Prakash ;; Maintainer: Praki Prakash ;; Created: 2017-12-31 ;; Keywords: languages ;; Homepage: https://MonadicT.gihub.io ;; This file is not part of GNU Emacs. ;;; Dependencies ---------------------- (require 'cl) (require 'subr-x) (require 's) (require 'yatl) ;;; Variables (require 'ox-html) ;;; Variables and options (defgroup org-export-blog nil "Options specific to RSS export back-end." :tag "Org Blog" :group 'org-export :version "24.4" :package-version '(Org . "9.0")) (defcustom blog-gen-publish-url "https://MonadicT.github.io" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-title "MonadicT" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-tagline "I see dead objects!" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-author "Praki Prakash" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-copyright-message "Copyright © 2014-%s, Praki Prakash" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-style-file "blog-style.css" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-banner-file "banner.org" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-footer-file "footer.org" "???" :group 'org-export-blog :type 'string) (defcustom blog-gen-root-dir "~/projects/MonadicT.github.io/" "") (defcustom blog-gen-posts-dir "_resources/posts/" "") (setq blog-gen-local t) ;;; Functions ------------------------ (defmacro assert-equal (actual expected) `(when (not (equal ,actual ,expected)) (error "Actual: %s expected: %s" (format "%s" ',actual) (format "%s" ',expected)))) (defun blog-gen-get-all-keywords() (org-element-map (org-element-parse-buffer 'element) 'keyword (lambda (kw) (cons (org-element-property :key kw) (org-element-property :value kw))))) (defun blog-gen-get-keyword-value(keywords key &optional default-value) (if-let ((kw-value (cdr (assoc-ignore-case key keywords)))) kw-value default-value)) (defun blog-gen-post-details (f) (with-temp-buffer (find-file f) (let* ((kws (blog-gen-get-all-keywords)) (details (make-hash-table :test #'equal)) (export-file-name (blog-gen-get-keyword-value kws "export_file_name" nil)) (href (or export-file-name ""))) (while (string-match "^\\.\\./" href) (setq href (substring href 3))) (puthash "post-file" f details) (puthash "title" (blog-gen-get-keyword-value kws "title" "") details) (puthash "summary" (blog-gen-get-keyword-value kws "summary" "") details) (puthash "publish-date" (blog-gen-get-keyword-value kws "publish-date" nil) details) (puthash "export_file_name" export-file-name details) (puthash "tags" (blog-gen-get-keyword-value kws "tags" "") details) (puthash "target" (blog-gen-get-keyword-value kws "target" "") details) (puthash "href" (concat (blog-gen-base-url) href) details) (unless (string-match "blog-generator.org" f) (kill-buffer)) details))) (defun blog-gen-post-files() (let* ((posts-dir (concat blog-gen-root-dir "/" blog-gen-posts-dir)) (org-files (directory-files posts-dir t "[a-ZA-Z0-9_-]*\\.org$")) (org-files (seq-remove (lambda (f) (or (string-match "index.org$" f) (string-match "about.org$" f) (string-match "sitemap.org$" f))) org-files))) (mapcar #'blog-gen-post-details org-files))) (defun blog-gen-published-posts (posts) (seq-filter (lambda (p) (gethash "export_file_name" p)) posts)) (defun blog-gen-order-posts(posts) (seq-sort (lambda (a b) (string> (gethash "publish-date" a) (gethash "publish-date" b))) posts)) (defmacro html-export(&rest content) `(progn (insert "#+BEGIN_EXPORT html\n") (insert ,@content) (insert "\n#+END_EXPORT\n\n"))) (defun blog-gen-create-posts() (let ((posts (blog-gen-published-posts (blog-gen-post-files)))) (mapcar (lambda (post) (let ((post-file (gethash "post-file" post)) (export-file-name (gethash "export_file_name" post))) (message (concat "exporting" post-file "to" export-file-name)) (when export-file-name (with-temp-buffer (find-file post-file) (org-export-to-file 'blog export-file-name) (kill-buffer))))) posts))) (defun blog-gen-create-articles() (with-temp-buffer (find-file (concat blog-gen-root-dir "index.org")) (erase-buffer) (insert "#+title: MonadictT\n") (insert "#+options: num:nil html-style:nil\n") (insert "#+HTML_HEAD: <link href=\"https://fonts.googleapis.com/css?family=Cormorant+Garamond|Roboto\" rel=\"stylesheet\">\n") (insert "#+HTML_HEAD_EXTRA: <style>* {font-family: 'Roboto';}</style>\n") (insert "#+HTML_HEAD_EXTRA: <style>pre {font-family: 'Segoe Print';}</style>\n") (insert "* Posts\n") (let ((posts (blog-gen-order-posts (blog-gen-published-posts (blog-gen-post-files))))) (mapcar (lambda (post) (let* ((title (gethash "title" post)) (summary (gethash "summary" post)) (export-file-name (gethash "export_file_name" post)) (export-file-name (let ((href export-file-name)) (while (string-match "^\\.\\./" href) (setq href (substring href 3))) href)) (publish-date (gethash "publish-date" post)) (l (list (make-symbol (format "a@href=\"/%s\"" export-file-name)) title))) (html-export (yatl-html-frag (div.post-title (eval (yatl-compile-fn l))))) (html-export (yatl-html-frag (div.post-summary summary)) "\n\n") (html-export (yatl-html-frag (div.post-publish-date "Published: " publish-date)) "\n"))) posts)) (save-buffer) (let ((out-file (concat blog-gen-root-dir "articles.html"))) (when (file-exists-p out-file) (delete-file out-file)) (org-export-to-file 'blog out-file)) (delete-file (concat blog-gen-root-dir "index.org")))) ;;(blog-gen-create-articles) (defun create-tag-post-map(posts) (let ((tag-post-map (make-hash-table :test 'equal))) (mapcar (lambda(post) (let ((title (gethash "title" post)) (tags (mapcar #'s-trim (split-string (gethash "tags" post) "[,]+")))) (mapcar (lambda (tag) (when (not (string-empty-p tag)) (puthash tag (cons post (gethash tag tag-post-map '())) tag-post-map))) tags))) posts) tag-post-map)) (defun sorted-keys(tag-post-map) (let ((keys '())) (maphash (lambda (k v) (setq keys (cons k keys))) tag-post-map) (seq-sort (lambda (a b) (string-lessp (upcase a) (upcase b))) keys))) (defun blog-gen-create-tags(posts) (let* ((tag-post-map (create-tag-post-map posts)) (tags (sorted-keys tag-post-map))) (with-temp-buffer (find-file "tags.org") (erase-buffer) (insert "#+title: Tags\n") (insert "#+options: num:nil\n") (insert "* Tags\n") (mapcar (lambda (tag) (let ((post-list (gethash tag tag-post-map))) (insert "** " tag "\n") (mapcar (lambda (post) (princ post) (let ((title (gethash "title" post)) (href (gethash "href" post))) (insert "- [[" href "][" title "]]\n"))) post-list))) tags) (write-file "tags.org") (let ((out-file (concat blog-gen-root-dir "tags.html"))) (org-export-to-file 'blog out-file)) (kill-buffer)))) ;; (setq x (blog-gen-post-files)) ;; (blog-gen-create-tags x) ;; (require 'blog-gen) (defun blog-gen-create-about() (with-temp-buffer (find-file (concat blog-gen-root-dir blog-gen-posts-dir "about.org")) (next-line) ;; When on line 1, org export throws a weird error (org-export-to-file 'blog (concat blog-gen-root-dir "about.html") nil ) (kill-buffer))) (blog-gen-create-about) (defun blog-gen-create-home() (copy-file (concat blog-gen-root-dir "articles.html") (concat blog-gen-root-dir "index.html") t)) (defun blog-gen-publish(prod) (interactive "P") (message "Blog generattion started") (if prod (setq blog-gen-local nil) (setq blog-gen-local t)) (message (format "Local Gen %S" blog-gen-local)) (let ((posts (blog-gen-post-files))) (blog-gen-create-posts) (message "Created posts") (blog-gen-create-articles) (message "Created articles page") (blog-gen-create-tags posts) (message "Created tags page") (blog-gen-create-about) (message "Created about page") (blog-gen-create-home) (message "Created home page")) (when prod (when (blog-gen-file-contains-p "localhost") (error "localhost references in file")))) (defun blog-gen-file-contains-p(s) nil) (global-set-key (kbd "C-c C-g") #'blog-gen-publish) (defun blog-gen-new-post() (interactive) (insert "#+title: TBD #+summary: TBD #+publish-date: 2018-01-31 #+export_file_name: ../../yyyy/TBD #+tags: TBD #+option: num:nil")) (provide 'blog-gen) ;;; blog-gen.el ends here