#+title: 7d.nz
#+author: Phil. Estival
* • [2021-07-11 dim.] Blogging with org :litterate:programming:org:elisp:
Here are the explanations and the source code of the
blogging engine developed for this website (7d.nz).
It's build with Emacs and org-mode,
creates static HTML pages and got inspired by ox-hugo.
While you reader can start reading it here,
you'll probably be more at ease under Emacs especially
when dwelling into the details of the code. Lucky for
you, every pages of this website provides the org source
its footer.
Update: [2024-06-02 dim.] Org 9.7.2
** License
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
** Quickstart :ATTACH:
One way to understand how it works could be to evaluate
the following code under Emacs, and then start blogging
right away just by modifying existing headlines or saving
new ones. Even safer and perhaps inspirational
will be to read it first.
Tested on emacs 28 to 30 and org 9.5 to 9.7.
Once autoexport is enabled
— and you can see it by having the following expression
"autoexport: enabled ?" highlighted —
saving the file when the cursor will be over a section
in a DONE state will export that section as a blog post.
All you need to run the static generator is a copy of this
file, assuming compatible versions of the libraries. The
main entry point to setup the blog is the following code
block wich in turns evaluate the legit elisp blocks down
below.
<<setup>>
#+name: org-blog
#+begin_src elisp :results silent
(message "setting up org-blog auto-export mode")
(save-excursion
(setq org-confirm-babel-evaluate
(lambda (lang body) (not (equal "elisp" lang))))
(condition-case err
(while (org-next-block-if-any)
(cond (
(looking-at "^#\\+begin_src +\\(elisp\\) *$")
(let (
(l (match-end 0)))
(search-forward "#+end_src")
(setq init-org-block (+ 1 init-org-block))
(princ " ")
(eval-region l (match-beginning 0))
))))
(error () (message "%s at line %s" (error-message-string err) (line-number-at-pos)))))
(org-blog-auto-export-mode)
(add-hook 'before-save-hook 'time-stamp)
(org-display-inline-images)
(setq htmlize-force-inline-images t)
(setq org-blog-regen-toc&tags nil)
#+end_src
org-next-block-if-any is defined in init.el.
to export the entry point TOC & tag list upon saving:
: (setq org-blog-regen-toc&tags t)
Turn it off if you're just publishing one article.
: (setq org-blog-regen-toc&tags nil)
keep
: org-blog-regen-toc&tags t
and
: (setq org-blog-regen-tags-p nil)
to update the front page without regenerating the tags pages.
** How to read this document
- online https://7d.nz/Blogging-with-org.html
- from an org-renderer of github or gitlab
https://gitlab.com/7dnz/org-weblog. Please note that
most online org renderers are still incomplete, and
some text will be missing.
- from the original sources in org and with your
prefered editor. You'll find them either in the
source repository, or by looking down below of
the HTML pages: 7d.nz/org/org-weblog.
Enjoy the reading and good luck with the coding
part. If you go into the details, you may noticed
a few inconsistancies in the function prefixes
(org-blog, org-weblog, blog-), as I haven't stated
yet on the definitive naming.
** Introduction :0:intro:
After having tried many of the blogging engines available
from org-mode, I ended with the following list of
specifications:
- The entire blog should be kept in one file with one
headline as a blog entry. The first time I read about this
idea was on Arthur Malabarba's blog: Endless Parenthesis
(This is also the place I landed on when I first searched
init.org). Endless Parentheses uses ox-jekyll.
- One article per top level section. While it should
also be possible to manage multiple blogs, with one
blog per top level section, and one article per level
2 section.
- Only Emacs, no other dependencies, static and minimalist
design.
- The publishing process should be as fast, automatized
and seamless as possible, exporting only what is needed
to render one article at a time, located by
the cursor position.
- The closest thing I found to what I was looking for
was Kaushal's scripter.co with ox-hugo. this
blogging system is neat. It's one of the best that's
been around and the org-mode source is clean.
However his setup file is big. The guy is connected
to dozens of social medias and I didn't start from
the bare config but from his own personnal
repository. Much tweaking is required to dig into
hugo and go-templates (btw I don't have any proper
syntax coloring for this). Hugo is a good solid
blogging engine, and it certainly does provide much
more than what I'll get here all by myself, but still...
- [ ] [2023-06-13 mar.] one addendum to the specifications
was recently suggested to me by email: we should define a
template property, and an org-weblog-templates alist that
holds all the different templates styles parameters. The
next step would be to externalize the styling along with
the author signature.
- The next version of the org-weblog should invoke
org-publish.
In the end, deriving the html exporter and programming
it's behavior turned out to be the way to go.
The general idea activated here is a literate
programming flow. That flow combines two aspects in the
work:
- the programming part, which I'll explain in
detail here, since this is the place where the code is
written, commented and elaborated,
- The literate part, that is the
discipline one has to keep up in order to organize
ideas and thoughts and communicate them.
And now that the blog is ready, I can finally communicate
more about past, present and future software projects and
the many aspects of programming without bothering too much
about the technicalities.
The next chapter that will also gets integrated into
this blog is about collective review. That's still
work in progress at the time I'm writing this [2021-12-20 lun.],
and I'm getting a bit late on the topic.
Literate programming is about programming and
literature. You have to consider that programming is
indeed a kind of literature, and that informatics is
all about text, much more than numbers: the mere
existence of transistors, electricity and 0 and 1 is
99% of the time of no concern to programmers — and, as a
matter of fact, to anyone else — unlike the texts
they're working on.
The publishing medium matters here because the program
is all about publishing online.
** Structure
./img/jumpb2.png
One headline = one post.
An org headline starts like this
: * TODO [#3] [2021-05-24 Mon 23:07] blogging with org-mode :code:article:post:
But Many times it will be simpler than that:
: * [2021-07-20 Tue] News from the front
This will produce a new post in a file named after a
"slug" from the
headline. If needed, that filename can be explicitly
defined with the EXPORT_FILE_NAME property. This has
however a drawback when using stored links, as they
reference the original file, but after, in the export.
Top level entries are sometimes called "headline level
1", sometimes, "H1", even this can be confused with
HTML. That behaviour can be overriden if one really
needs to store the blog posts under one section.
The state (TODO/DONE) determines if a post must
be published or not.
When in a DONE state (such as a square ■, in this
document, according to the top directives) every time C-x C-s
is hit while the cursor is in the section, it is
exported, while the summary and the index.org gets
updated accordingly.
The timestamp headlines order the post chronologically
in the blog. The tags are also exported for grouping
and categorization. Priorities are ignored.
The index page is a list of blog entries with a short
summary of the articles.
So, once the auto-export in enabled,
writing in org file will export the subtree
to it's own org-file, which in turns gets
exported to html.
The Html backend exporter still doesn't handle the case
of a few fuzzy links, so we will have to take care of
that.
Pagination isn't implemented yet. A simple index
increment from url /page/1 to /n would do. /page/1
might be a special case if it's also the landing page
but it won't be necessarly the "recent posts" list on
the front page (maybe I'll keep some on top headline,
or even use the org list for this order).
*** UP heading
The point has to return to a headline in order to get
the information about the section or sub-section we're
currently writing in.
#+begin_src elisp
(defun org-up-heading (level)
"return up to closest headline at LEVEL"
(condition-case err
(outline-up-heading
(- (nth 1 (org-heading-components))
level))
(error () nil)))
(defun heading-components-at-level (level)
"return the top level heading components
of the current section. Said differently : get back to the
root (level 1) of the current section, down to n level"
(save-excursion
(condition-case err
(outline-up-heading
(- (nth 1 (org-heading-components))
level))
(error () nil)) (org-heading-components)))
#+end_src
: (org-up-heading 1)
: (org-up-heading 3)
: (heading-components-at-level 1)
** Date/title extraction
We separate the date (if any) and the title. When's
there's no date (at least, no detected date — that is
when the regexp didn't match), the post in considered
as holding only a title, and will be set to the
current date and time when an export occurs.
#+begin_src elisp
(defconst org-timestamp-and-title
"\\(\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\) +\\([^]+0-9>
-]+\\)?\\( +\\([0-9]\\{1,2\\}:[0-9]\\{2\\}\\)\\)?\\)] \\(.*\\)")
(defun split-datetime+title (str)
(list :timestamp (match-string 1 str)
:year (match-string 2 str)
:month (match-string 3 str)
:time (match-string 7 str)
:title (match-string 8 str)))
(defun timestamp-and-title (str)
""" on a bare string return current time + string """
(let ((time) (current-time))
(if (string-match org-timestamp-and-title str)
(split-datetime+title str)
(list :timestamp (format-time-string "[%Y-%m-%d %a %H:%M]" time)
:year (format-time-string "%Y" time)
:month (format-time-string "%m" time)
:time (format-time-string "%H:%M" time)
:title str))))
(defun org-html-title-only (str)
(if (string-match org-timestamp-and-title str)
(split-datetime+title str)
(str)))
#+end_src
Tests :
#+begin_src elisp
(format-time-string "[%Y-%m-%d %H:%M]" (current-time))
(timestamp-and-title "log.el.org")
(timestamp-and-title (nth 4(heading-components-at-level 1)))
(plist-get (timestamp-and-title (nth 4 (heading-components-at-level 1))) :timestamp)
(plist-get (timestamp-and-title (nth 4 (heading-components-at-level 1))) :title)
(timestamp-and-title "[2021-07-11 Sun] blog.el.org")
(timestamp-and-title "[2021-07-11 Lun.] blog.el.org")
(timestamp-and-title "[2021-07-11 Sun 00:00] log.el.org")
#+end_src
** Slugs
let's take a look at a Python "sluggifier". A
better implementation with i18n support certainly
exists in Django.
#+begin_src python
s = s.lower()
acct = "ãàáäâẽèéëêìíïîõòóöôùúüûñç"
wacct = "aaaaaeeeeeiiiiooooouuuunc"
for i in range(len(acct)):
s = s.replace(acct[i], wacct[i])
s = s.replace("œ", "oe")
acct = "-/_,:;."
for i in range(len(acct)):
s = s.replace(acct[i], " ")
s = re.sub('\s+', ' ', s)
s = s.strip()
s = s.replace(' ', '-')
s = re.sub('\W', '-', s)
return s.strip('-')
#+end_src
*** intermediate functions
#+begin_src elisp
(defun replace-chars-in-string (set-1 with-set-2 str)
"Replace index-wise the character from set 1 in str by those of set 2 inside a string"
(dotimes (i (length set-1))
(setq str (string-replace
(char-to-string (aref set-1 i))
(char-to-string (aref with-set-2 i))
str)))
str)
#+end_src
#+begin_src elisp
(defun replace-chars (set-1 char-2 str)
"Replace the character from set 1 in str with char-2"
(dotimes (i (length set-1))
(setq str (string-replace
(char-to-string (aref set-1 i))
char-2
str)))
str)
#+end_src
: (char-to-string 231)
#+begin_src emacs-lisp
(replace-chars-in-string
"ãàáäâẽèéëêìíïîõòóöôùúüûñç"
"aaaaaeeeeeiiiiooooouuuunc" "[some] _ àrticle's_titlé--")
(replace-chars
"ãàáäâẽèéëêìíïîõòóöôùúüûñç"
"X" "[some] _ àrticle's_titlé--")
#+end_src
*** Sluggyfier
#+begin_src elisp
(defun sluggify (str)
(replace-regexp-in-string
"-+" "-" (replace-regexp-in-string
"^-+" "" (replace-regexp-in-string
"-+$" "" (replace-chars-in-string
"/_,:;."
"------"
(replace-regexp-in-string
(regexp-quote "\s") "-"
(replace-chars-in-string
"[](){}'"
"-------" (replace-chars-in-string
"ãàáäâẽèéëêìíïîõòóöôùúüûñç"
"aaaaaeeeeeiiiiooooouuuunc" str))))))))
#+end_src
A slightly better option would look like so
#+begin_src elisp
(defun sluggif1 (str)
(let((replacements
'(
("[](){}'" . "-")
("/_,:;." . "-")
("-+$" . "") ("^-+" . "") ("-+" . "-") ))
(result))
(setq str(replace-chars-in-string "ãàáäâẽèéëêìíïîõòóöôùúüûñç"
"aaaaaeeeeeiiiiooooouuuunc" str))
(cl-loop for (key . value) in replacements
collect (setq str (replace-chars key value str)))))
#+end_src
** HTML backend derivation
I'm using the standard html backend derivation
file:/usr/share/emacs/28.0.5/straight/repos/org/lisp/ox-html.el
negating a few options to get it to a bare minimum.
translate-alist indicates what function
is in charge of translating each org elements.
*** Checking if a headline is already in the toc
Html ids are unique, but headlines are not.
Add a number counter to differentiate identical headlines
#+begin_src elisp
(defvar headline-list '() "the list of all headlines met during one export")
#+end_src
Generalized to this function:
#+begin_src elisp
(defun append-next-numeral (string-list item)
"add a numeral to `ITEM` present in the list `STRINGLIST`
until it's unique, then add it to the list"
(let( (crnt item) (i 0))
(while (member crnt string-list)
(setq crnt (concat item (format "-%s" (setq i (+ i 1))))))
crnt))
#+end_src
(setq headline-list nil)
That we use this way:
#+begin_src emacs-lisp
(add-to-list 'headline-list (append-next-numeral headline-list "title"))
#+end_src
*** Headline
As described at the begining, headlines follow this structure :
#+begin_example
,* TODO [#3] [2021-05-24 Mon 23:07] blog.el.org :dev:article:post:
#+end_example
For section of level 1 and more the title and the tags needs to
be exported, state and priority are discarded. Date is
displayed in its own part.
Here's a derivation from org-html-headline.
#+begin_src elisp
(defun exporting-article-p (info) (plist-get info :export-article))
(defun org-blog-headline (headline contents info)
"Transcode a HEADLINE element from Org to HTML.
CONTENTS holds the contents of the headline. INFO is a plist
holding contextual information."
(unless (org-element-property :footnote-section-p headline)
(let* ((numberedp nil) (numbers (org-export-get-headline-number headline info))
(level (+ (org-export-get-relative-level headline info)
(1- (plist-get info :html-toplevel-hlevel))))
(todo (and (plist-get info :with-todo-keywords)
(let ((todo (org-element-property :todo-keyword headline)))
(and todo (org-export-data todo info)))))
(todo-type (and todo (org-element-property :todo-type headline)))
(priority (and (plist-get info :with-priority)
(org-element-property :priority headline)))
(title (org-export-data (org-element-property :title headline) info))
(tags (and (plist-get info :with-tags)
(org-export-get-tags headline info)))
(tags-elem (if tags
(format " <div class='tags'>%s</div>"
(mapconcat (lambda(c)(format "<a href='%s.html'>%s</a>" c c))
tags " "))
""))
(full-text (funcall (plist-get info :html-format-headline-function)
todo todo-type priority title tags info))
(contents (or contents ""))
(id (replace-regexp-in-string
"['\" ]" "-"
(or (org-element-property :CUSTOM_ID headline)
(org-export-get-reference headline info))
(org-export-get-reference headline info)))
(formatted-text
(if (plist-get info :export-article)
(format "<a href='#%s'>%s</a>" id full-text)
(format "<a href='%s'>%s</a>" (sluggify full-text) full-text))))
(setq title-num (append-next-numeral
headline-list
(replace-regexp-in-string "['\" ]" "-" title)))
(add-to-list 'headline-list title-num)
(if (org-export-low-level-p headline info)
(let* ((html-type (if numberedp "ol" "ul")))
(concat
(and (org-export-first-sibling-p headline info)
(apply #'format "<%s class=\"org-%s\">\n"
(make-list 2 html-type)))
(org-html-format-list-item
contents (if numberedp 'ordered 'unordered)
nil info nil
(concat (org-html--anchor id nil nil info) formatted-text)) "\n"
(and (org-export-last-sibling-p headline info)
(format "</%s>\n" html-type))))
(let* ((extra-class
(org-element-property :HTML_CONTAINER_CLASS headline))
(headline-class
(org-element-property :HTML_HEADLINE_CLASS headline))
(first-content (car (org-element-contents headline))))
(when(and tags
(> level 1)
(plist-get info :export-article))
(message "exporting inner tags")
(blog-ref-inner-tags (org-export-output-file-name "") tags)
(message "exporting inner tags DONE"))
(format "<%s class='%s'>%s%s</%s>\n" (org-html--container headline info)
(concat (format "section-%d" level)
(and extra-class " ")
extra-class)
(if (exporting-article-p info)
(format "\n<h%d id='%s'><a href='#%s'> %s </a>%s</h%d>\n"
level
title-num
title-num
title
tags-elem
level)
(format "\n<h%d id='%s'><span class='timestamp'>%s</span> <a href='%s.html'>%s</a>%s</h%d>\n"
level title-num
(or (org-element-property :DATE headline) "")
(or (org-element-property :EXPORT_FILE_NAME headline)
(sluggify title)) title
tags-elem
level))
(if (eq (org-element-type first-content) 'section) contents
(concat (org-html-section first-content "" info) contents))
(org-html--container headline info)))))))
#+end_src
- [2021-09-02 jeu. 15:10] (C-c ok, mais unbalanced with C-x e ...
seems related to the pairing of "< >" along the parentheses.
got to fix this.
#+begin_src elisp
(setq org-export-headline-levels 4)
#+end_src
[2023-06-18 dim.]I see yet an other in a bug
*** Source code block
Add a class for prism.js.
#+begin_src elisp
(defun src-code-block (src-block contents info)
"Transcode a SRC-BLOCK element from Org to ASCII.
CONTENTS is nil. INFO is a plist used as a communication
channel."
(concat
(format "<pre class='code line-numbers'>
<code class='language-%s'>%s</code><button class='expand'>expand</button></pre>"
(org-element-property :language src-block)
(replace-regexp-in-string
"\"" """ (org-html-encode-plain-text
(org-element-normalize-string
(org-export-format-code-default src-block info)))))))
#+end_src
The proper way to go would be to stack the detected
programming languages in the current page and insert
the relevant css for each of them.
Prism current configuration :
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+awk+bash+c+css-extras+go+graphql+latex+lisp+makefile+mongodb+python+regex+rust+sql+typescript&plugins=line-highlight+line-numbers+inline-color+normalize-whitespace+toolbar+copy-to-clipboard+treeview
Total filesize: 71.3KB (84% JavaScript + 16% CSS)
*** TODO Statistic cookie
This is handled fine with org-js.
It needs special care to insert formatting
that doesn't clutter the links in titles.
#+begin_src elisp
(defun blog-html-statistics-cookie (statistics-cookie _contents _info)
"[25/100]"
(let ((cookie-value (org-element-property :value statistics-cookie)))
(format "%s" cookie-value)))
#+end_src
*** Org html template head & body
Blog title is displayed in every page.
#+begin_src elisp
(defcustom org-weblog-title
"7d.nz"
"The name of the blog" :group 'org-weblog)
#+end_src
<meta></meta>
#+begin_src elisp
(setq org-html-validation-link "
<a href='https://creativecommons.org/licenses/by-sa/4.0/deed.en'><img src='img/button-cc-by-sa.png' alt='cc-by-sa'></a>
<a href='https://orgmode.org'><img src='img/button-orgmode.png' alt='orgmode'></a>
<a href='https://validator.w3.org/check?uri=referer'><img src='img/button-html5.png' alt='html5'></a>")
#+end_src
#+begin_src elisp
(defun org-blog--build-meta-info (info)
"Return meta tags for exported document.
INFO is a plist used as a communication channel."
(message "meta")
(let* ((protect-string
(lambda (str)
(replace-regexp-in-string
"\"" """ (org-html-encode-plain-text str))))
(title (or (nth 2 (plist-get info :title)) (car (plist-get info :title))))
(title (if (org-string-nw-p title) title " "))
(author (nth 1 (car(org-collect-keywords '("AUTHOR")))))
(description (plist-get info :description))
(keywords (plist-get info :keywords))
(charset (or (and org-html-coding-system
(fboundp 'coding-system-get)
(coding-system-get org-html-coding-system
'mime-charset))
"utf-8")))
(concat
(when (plist-get info :time-stamp-file)
(format-time-string
(concat "<!-- "
(plist-get info :html-metadata-timestamp-format)
" -->\n")))
(format
(if (org-html-html5-p info)
(org-html-close-tag "meta" "charset=\"%s\"" info)
(org-html-close-tag
"meta" "http-equiv=\"Content-Type\" content=\"text/html;charset=%s\""
info))
charset) "\n"
(let ((viewport-options
(cl-remove-if-not (lambda (cell) (org-string-nw-p (cadr cell)))
(plist-get info :html-viewport))))
(and viewport-options
(concat
(org-html-close-tag
"meta name='viewport'"
(format "content='%s'"
(mapconcat
(lambda (elm) (format "%s=%s" (car elm) (cadr elm)))
viewport-options ", "))
info)
"\n")))
(format "<title>%s%s</title>\n" org-weblog-title
(if (s-equals? title org-weblog-title) ""
(concat " — " title) ))
(org-html-close-tag "meta" "name='generator' content='Org-mode'" info)
"\n"
(and (org-string-nw-p author)
(concat
(org-html-close-tag "meta"
(format "name='author' content='%s'"
(funcall protect-string author))
info)
"\n"))
(and (org-string-nw-p description)
(concat
(org-html-close-tag "meta"
(format "name='description' content='%s'\n"
(funcall protect-string description))
info)
"\n"))
(and (org-string-nw-p keywords)
(concat
(org-html-close-tag "meta"
(format "name='keywords' content='%s'"
(funcall protect-string keywords))
info)
"\n")))))
(defun org-blog-inner-template (contents info)
"Return body of document string after HTML conversion.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options."
(concat
"<a href='/' class='homebtn'><img src='img/7dnzbw.png'/></a>"
"<img id='toggle-theme' src='img/toggle-theme.png'>"
(let ((depth (plist-get info :with-toc)))
(when depth (org-blog-toc depth info)))
"<article" (when (not (exporting-article-p info)) " class='index'" ) ">"
contents
(org-html-footnote-section info)
"</article>" ))
(defun org-blog-template (contents info)
"Return complete document string after HTML conversion.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options."
(concat
(when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
(let* ((xml-declaration (plist-get info :html-xml-declaration))
(decl (or (and (stringp xml-declaration) xml-declaration)
(cdr (assoc (plist-get info :html-extension)
xml-declaration))
(cdr (assoc "html" xml-declaration))
"")))
(when (not (or (not decl) (string= "" decl)))
(format "%s\n"
(format decl
(or (and org-html-coding-system
(fboundp 'coding-system-get)
(coding-system-get org-html-coding-system 'mime-charset))
"iso-8859-1"))))))
(org-html-doctype info)
"\n"
(concat "<html"
(cond ((org-html-xhtml-p info)
(format
" xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
(plist-get info :language) (plist-get info :language)))
((org-html-html5-p info)
(format " lang=\"%s\"" (plist-get info :language))))
">\n")
"<head>\n"
(org-blog--build-meta-info info)
(org-html--build-head info)
(let ((link-up (org-trim (plist-get info :html-link-up)))
(link-home (org-trim (plist-get info :html-link-home))) (title (plist-get info :title)))
(unless (and (string= link-up "") (string= link-home ""))
(format (plist-get info :html-home/up-format)
(or link-up link-home)
(or link-home link-up))))
(org-html--build-pre/postamble 'preamble info)
"</head>\n<body>
<div id='top-line'></div>
<div class='modal'></div>"
(let ((div (assq 'content (plist-get info :html-divs))))
(format "<%s id=\"%s\">\n" (nth 1 div) (nth 2 div)))
(when (plist-get info :with-title) (let ((title (and (plist-get info :with-title)
(plist-get info :title)))
(subtitle (plist-get info :subtitle))
(html5-fancy (org-html--html5-fancy-p info))
(tags (plist-get info :title-tags)))
(format "<h1 class='page-title'><a href='index.html'>%s</a></h1>" org-weblog-title)
(when title
(format
(concat
"<header" (when (exporting-article-p info) " class='article'" ) ">
<h1 class='page-title'><a href='index.html'>
<img src='img/7dnz.png' alt='%s'></a>
</h1>
</header>
<div class='title" (when (not (exporting-article-p info)) " index" ) "'>
<h1 class='title'>%s</h1>
<h2 class='timestamp'>%s</h2>
%s
</div>")
org-weblog-title
(or (nth 2 title) (car title))
(or (plist-get (nth 1(nth 1 title)) :raw-value) " ")
(if tags
(format " <span class='tags'>%s</span>"
(mapconcat
(lambda (c)
(and (not (string-empty-p c))
(format "<a href='%s.html'>%s</a>" c c)))
(split-string tags ":") " "))
"")
(if subtitle
(format
(if html5-fancy
"<p class=\"subtitle\">%s</p>\n"
(concat "\n" (org-html-close-tag "br" nil info) "\n"
"<span class=\"subtitle\">%s</span>\n"))
(org-export-data subtitle info))
"")))))
contents
(format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
"<footer>"
(when (exporting-article-p info)
(let ((o (sluggify title)))
(format "<div class='prepostamble'>
<p class='orgfile'><a href='org/%s.org'>%s.org</a></p>
<p class='orgfile'><a href='org/%s.org.html'>%s.org.html</a></p>
</div>" o o o o)))
(org-blog--build-pre/postamble 'postamble info)
" <div class='footer-icons'>
<a href='http://github.com/flintforge'><img src='img/gh-mini.png' alt='github/flintforge'></a>
<a href='http://gitlab.com/7dnz'><img src='img/gitlab.svg' alt='gitlab/7d.nz'></a>
<a href='./index.xml'><img src='img/rss.png' alt='rss'></a>
<span id='hits'></span>
<a href='./index.html'>
<img class='licence' src='./img/footlogo.png' alt='7dnz'>
</a>
</div>"
"</footer>"
"<div id='bottom-line'></div>"
"</body>\n</html>"))
#+end_src
The TOC highlighter is here here : file:./js/script.js
The title when given to the exporter
is a list of strings : TODO, priority, timestamp and title,
the timestamp beeing an org-element
#+begin_example
(⬛ [#A] (timestamp (:type inactive :raw-value [2021-07-11 Sun]
:year-start 2021 :month-start 7 :day-start 11 :hour-start nil
:minute-start nil :year-end 2021 :month-end 7 :day-end 11 :hour-end
nil :minute-end nil :begin 8 :end 25 :post-blank 1 :parent #0))
blog.el.org)
#+end_example
Without a date, this is what it will looks like
#+begin_example
(⬛ [#A] blog.el.org)
#+end_example
*** TODO customize the postamble
date, author,
validation link,
#+begin_src elisp
(setq org-html-postamble-format
'("en" "<p class=\"author\">Author: %a (%e)</p>
<p class=\"date\">Date: %d</p>
<p class=\"creator\">%c</p>
<p class=\"validation\">%v</p>"))
#+end_src
#+begin_src elisp
(defun org-blog--build-pre/postamble (type info)
"Return document preamble or postamble as a string, or nil.
TYPE is either `preamble' or `postamble', INFO is a plist used as a
communication channel."
(let ((section (plist-get info (intern (format ":html-%s" type))))
(spec (org-html-format-spec info)))
(when section
(let ((section-contents
(if (functionp section) (funcall section info)
(cond
((stringp section) (format-spec section spec))
((eq section 'auto)
(let ((date (cdr (assq ?d spec)))
(author (cdr (assq ?a spec)))
(email (cdr (assq ?e spec)))
(creator (cdr (assq ?c spec)))
(validation-link (cdr (assq ?v spec))))
(concat
(and (plist-get info :with-date)
(org-string-nw-p date)
(format "<p class=\"date\">%s: %s</p>\n"
(org-html--translate "Date" info)
date))
(and (plist-get info :with-author)
(org-string-nw-p author)
(format "<p class=\"author\">%s: %s</p>\n"
(org-html--translate "Author" info)
author))
(and (plist-get info :with-email)
(org-string-nw-p email)
(format "<p class=\"email\">%s: %s</p>\n"
(org-html--translate "Email" info)
email))
(and (plist-get info :time-stamp-file)
(format
"<p class=\"date\">%s: %s</p>\n"
(org-html--translate "Last update" info)
(format-time-string
(plist-get info :html-metadata-timestamp-format))))
(and (plist-get info :with-creator)
(org-string-nw-p creator)
(format "<p class=\"creator\">%s</p>\n" creator))
(and (org-string-nw-p validation-link)
(format "<p class=\"validation\">%s</p>\n"
validation-link)))))
(t
(let ((formats (plist-get info (if (eq type 'preamble)
:html-preamble-format
:html-postamble-format)))
(language (plist-get info :language)))
(format-spec
(cadr (or (assoc-string language formats t)
(assoc-string "en" formats t)))
spec)))))))
(let ((div (assq type (plist-get info :html-divs))))
(when (org-string-nw-p section-contents)
(concat
(format "<%s id=\"%s\" class=\"%s\">\n"
(nth 1 div)
(nth 2 div)
org-html--pre/postamble-class)
(org-element-normalize-string section-contents)
(format "</%s>\n" (nth 1 div)))))))))
#+end_src
*** TOC toc
(add-to-list 'headline-list (append-next-numeral headline-list "title"))
#+begin_src elisp
(defun org-blog-toc (depth info &optional scope)
"Build a table of contents.
DEPTH is an integer specifying the depth of the table. INFO is
a plist used as a communication channel. Optional argument SCOPE
is an element defining the scope of the table. Return the table
of contents as a string, or nil if it is empty."
(setq headline-list nil) (let ((toc-entries
(mapcar (lambda (headline)
(cons (org-blog--format-toc-headline headline info) (org-export-get-relative-level headline info)))
(org-export-collect-headlines info depth scope))))
(when toc-entries
(let ((toc (concat
"<a id='toc-button'>"
"<svg viewBox='-5 0 10 8' width='30'> <line y2='8' stroke='#000' stroke-width='10' stroke-dasharray='2 1'></line>"
"</svg></a>"
"<nav id='text-TOC'>"
(org-html--toc-text toc-entries)
"</nav>"
"\n")))
(if scope toc
(let ((outer-tag (if (org-html--html5-fancy-p info) "nav" "div")))
(concat (format "<%s id='TOC' class='toc'>\n" outer-tag)
(let ((top-level (plist-get info :html-toplevel-hlevel)))
)
toc
(format "</%s>\n" outer-tag))))))))
#+end_src
#+begin_src elisp
(defun org-blog--format-toc-headline (headline info)
"Return an appropriate table of contents entry for HEADLINE.
INFO is a plist used as a communication channel."
(let* ((headline-number (org-export-get-headline-number headline info))
(todo (and (plist-get info :with-todo-keywords)
(let ((todo (org-element-property :todo-keyword headline)))
(and todo (org-export-data todo info)))))
(todo-type (and todo (org-element-property :todo-type headline)))
(priority (and (plist-get info :with-priority)
(org-element-property :priority headline)))
(text (string-replace "\"" """
(org-export-data-with-backend
(org-export-get-alt-title headline info)
(org-export-toc-entry-backend 'html)
info)))
(headline-x (append-next-numeral
headline-list
(replace-regexp-in-string "['\" ]" "-" text)))
(tags (and (eq (plist-get info :with-tags) t)
(org-export-get-tags headline info))))
(add-to-list 'headline-list headline-x)
(if (plist-get info :export-article)
(format "<a href=\"#%s\">%s</a>"
headline-x
(concat
text
))
(format "<a href='%s.html'>%s</a>" (sluggify text) text)
)))
#+end_src
*** node properties
#+begin_src emacs-lisp
(defun org-html-node-property (node-property _contents _info)
"Transcode a NODE-PROPERTY element from Org to HTML.
CONTENTS is nil. INFO is a plist holding contextual
information."
(format "%s ??:%?? s"
(org-element-property :key node-property)
(let ((value (org-element-property :value node-property)))
(if value (concat " " value) ""))))
#+end_src
*** footnotes / sidenotes
When the screen is large enough, (or allowing a slide on the left)
fotnotes may be displayed as sidenotes
. The regular display should be kept, for proper display
on tablets or prints. However forward footnote references needs to be fixed.
#+begin_src elisp
(defun org-blog-sidenote (footnote-reference _contents info)
"Transcode a FOOTNOTE-REFERENCE element from Org to HTML.
CONTENTS is nil. INFO is a plist holding contextual information."
(concat
(let ((prev (org-export-get-previous-element footnote-reference info)))
(when (eq (org-element-type prev) 'footnote-reference)
(plist-get info :html-footnote-separator)))
(let* ((n (org-export-get-footnote-number footnote-reference info))
(sid (format "sfn.%d" n)) (ids (format "fns.%d" n)) (idr (format "fnr.%d" n)) (id (format "fn.%d%s"
n
(if (org-export-footnote-first-reference-p footnote-reference info)
""
".100"))))
(concat
"<span class='sidenote'>"
(org-html--anchor sid n
(format " class='footnum' href='#fns.%d' role='doc-backlink'" n) info)
(org-export-data
(org-export-get-footnote-definition footnote-reference info) info)
"</span>"
(format
(plist-get info :html-footnote-format)
(org-html--anchor ids n
(format " class='footref fn-side' href='#sfn.%d' role='doc-backlink'" n) info))
(format
(plist-get info :html-footnote-format)
(org-html--anchor idr n
(format " class='footref fn-bottom' href='#fn.%d' role='doc-backlink'" n) info))
))))
#+end_src
para ?
bug The footnote text is considered as regular text (the same way the fontifiation occurs is emacs)
*** Derived backend definition
missing sections in the translation will make the
export only partial, but without any warning.
: (setq org-html-html5-fancy t)
#+begin_src elisp
(require 'ox-html)
(let ((org-export-before-parsing-hook '(org-ref-bbl-preprocess)))
(org-export-define-derived-backend 'weblog 'html
:options-alist
'((:html-head-include-default-style nil "html-style" nil)
(:html-head-include-scripts nil "html-scripts" nil)
(:html-head-include-scripts nil "html-scripts" nil)
(:html-doctype "HTML_DOCTYPE" nil "html5")
(:html-html5-fancy nil t t)
)
:translate-alist
'(
(drawer . nil) (src-block . src-code-block)
(inner-template . org-blog-inner-template)
(headline . org-blog-headline)
(template . org-blog-template)
(statistics-cookie . blog-html-statistics-cookie)
(link . org-blog-html-link)
(footnote-reference . org-blog-sidenote)
(comment-block . nil) )))
#+end_src
*** Preserve nbsp
#+begin_src elisp
(defun my-html-nobreak-space-filter (text backend info)
(and (org-export-derived-backend-p backend 'weblog)
(replace-regexp-in-string " " " " text)))
(add-to-list 'org-export-filter-plain-text-functions
#'my-html-nobreak-space-filter)
#+end_src
*** TODO paragraph
Need to override this one just to handle images
differently in ToC and article.
Consider using two exporters:
org-article-weblog
and org-toc-weblog.
it's using :html-self-link-headlines as marker
Is the html5-fancy also in a similar situation ?
#+begin_src elisp
(defun org-weblog-html--wrap-image (contents info &optional caption label)
"see org-html--wrap-image in ox-html.el
this adds minature management
"
(let ((html5-fancy (org-html--html5-fancy-p info)))
(format "\n"
(if html5-fancy
"<figure%s>\n%s%s\n</figure>"
"<div%s class=\"figure\">\n%s%s\n</div>")
(if (org-string-nw-p label) (format " id=\"%s\"" label) "")
(if html5-fancy contents (format "<p>%s</p>" contents))
(if (not (org-string-nw-p caption)) ""
(format (if html5-fancy "\n<figcaption>%s</figcaption>"
"\n<p>%s</p>")
caption)))))
#+end_src
#+begin_src elisp
(defun org-weblog-html-paragraph (paragraph contents info)
"Transcode a PARAGRAPH element from Org to HTML.
CONTENTS is the contents of the paragraph, as a string. INFO is
the plist used as a communication channel."
(let* ((parent (org-export-get-parent paragraph))
(parent-type (org-element-type parent))
(style '((footnote-definition " class=\"footpara\"")
(org-data " class=\"footpara\"")))
(attributes (org-html--make-attribute-string
(org-export-read-attribute :attr_html paragraph)))
(extra (or (cadr (assq parent-type style)) "")))
(cond
((and (eq parent-type 'item)
(not (org-export-get-previous-element paragraph info))
(let ((followers (org-export-get-next-element paragraph info 2)))
(and (not (cdr followers))
(memq (org-element-type (car followers)) '(nil plain-list)))))
contents)
((org-html-standalone-image-p paragraph info)
(let ((caption
(let ((raw (org-export-data
(org-export-get-caption paragraph) info))
(org-html-standalone-image-predicate
#'org-html--has-caption-p))
(if (not (org-string-nw-p raw)) raw
(concat "<span class=\"figure-number\">"
(format (org-html--translate "Figure %d:" info)
(org-export-get-ordinal
(org-element-map paragraph 'link
#'identity info t)
info nil #'org-html-standalone-image-p))
" </span>"
raw))))
(label (org-html--reference paragraph info)))
(org-weblog-html--wrap-image contents info caption label)))
(t (format "<p%s%s>\n%s</p>"
(if (org-string-nw-p attributes)
(concat " " attributes) "")
extra contents)))))
#+end_src
*** images
#+begin_src elisp
(defun org-blog-html-link(link desc info)
(if (plist-get info :export-article)
(org-html-link link desc info)
(org-string-nw-p desc))
)
#+end_src
*** Style.css
file:./css/style.css
#+begin_src elisp
(setq org-html-head"
<link rel='stylesheet' type='text/css' href='css/fonts/PT-sans.css'/>
<link rel='stylesheet' type='text/css' href='css/style.css'/>
<link rel='stylesheet' type='text/css' href='css/prism.css'/>
<script src='js/jquery.min.js' rel='preload'></script>
<script src='js/prism.js' rel='preload'></script>
<script src='js/script.js'></script>
<link rel='shortcut icon' href='favicon.gif'/>
")
#+end_src
#+results: #+begin_example
<link rel='stylesheet' type='text/css' href='css/fonts/PT-sans.css'/>
<link rel='stylesheet' type='text/css' href='css/style.css'/>
<link rel='stylesheet' type='text/css' href='css/prism.css'/>
<script src='js/jquery.min.js' rel='preload'></script>
<script src='js/prism.js' rel='preload'></script>
<script src='js/script.js'></script>
<link rel='shortcut icon' href='favicon.gif'/>
#+end_example
*** Head
For memo,
here is a <head>
#+begin_src html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5">
<meta name="referrer" content="no-referrer">
<script src='script.js'>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#ffc40d" />
<meta name="theme-color" content="#ffffff" />
<link href="https://github.com/7d.nz/" rel="me">
<meta property="og:title" content="" />
<meta property="og:description"
content=" …
" />
<meta property="og:type" content="article" />
<meta property="og:url" content="" />
<meta property="article:published_time" content="2020-11-02T00:00:00+01:00"/>
<meta property="article:modified_time" content="2020-11-02T00:00:00+01:00"/>
<meta name="twitter:card" content="summary"/><meta name="twitter:image" content="/android-chrome-256x256.png"/>
<meta name="twitter:title" content=""/>
<meta name="twitter:description" content=""/>
<meta name="DC.Creator" content="Phil.Estival"/>
#+end_src
** • Export to html: entry point :tagged:
./img/parachute.png
note that org-entry-get looks into org-special-properties first.
: (org-entry-get nil "FILE" t)
: (org-entry-get nil "TODO" t)
: (org-entry-get nil "ALLTAGS" t)
If an interactive export of the entire buffer is desired :
#+begin_src emacs-lisp
(defun org-blog-export-to-html
(&optional async subtreep visible-only body-only ext-plist)
"Export current buffer to a HTML file for a blog page.
If narrowing is active in the current buffer, only export its
narrowed part.
If a region is active, export that region.
A non-nil optional argument ASYNC means the process should happen
asynchronously. The resulting file should be accessible through
the `org-export-stack' interface.
When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.
When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.
When optional argument BODY-ONLY is non-nil, only write code
between \"<body>\" and \"</body>\" tags.
EXT-PLIST, when provided, is a property list with external
parameters overriding Org default settings, but still inferior to
file-local settings.
Return output file's name."
(message "export-to")
(interactive)
(let* (
(filename (or (org-entry-get nil "EXPORT_FILE_NAME" t)
(sluggify (plist-get (timestamp-and-title (nth 4 (heading-components-at-level 1))) :title) )))
)
(message "[ox-blog] exporting to html file %s.html" filename)
(org-export-to-file 'weblog (concat filename ".html")
async subtreep visible-only body-only ext-plist
)))
#+end_src
** Export subtree
This is from ox-hugo.el
#+begin_src emacs-lisp
(with-no-warnings
(if (fboundp #'org--get-local-tags) (defalias 'org-blog--get-tags 'org-get-tags)
(defalias 'org-blog--get-tags 'org-get-tags-at)))
#+end_src
Now, we aren't using any of these,
but they point out intresting display feature
that may be introduced later on.
#+begin_src emacs-lisp
(defun org-hugo--selective-property-inheritance ()
"Return a list of properties that should be inherited."
(let ((prop-list '( "DATE" "AUTHOR")))
(mapcar (lambda (str)
(concat "EXPORT_" str))
prop-list)))
#+end_src
ox-hugo.el::3904
Renamed it so there's no confusion if ox-blog is also active:
#+begin_src elisp
(defun org-blog--get-element-path (element info)
"Return the section path of ELEMENT.
INFO is a plist holding export options."
(let ((root (or (org-export-get-node-property :EXPORT_HUGO_SECTION element :inherited)
(plist-get info :hugo-section)))
(filename (org-export-get-node-property :EXPORT_FILE_NAME element :inherited))
(current-element element)
fragment fragments)
(while (and current-element
(not (org-export-get-node-property :EXPORT_HUGO_SECTION current-element nil)))
(when (setq fragment (org-export-get-node-property :EXPORT_HUGO_SECTION* current-element nil))
(push fragment fragments))
(setq current-element (org-element-property :parent current-element)))
(concat
(file-name-as-directory root)
(mapconcat #'file-name-as-directory fragments "")
filename)))
#+end_src
#+begin_src elisp
(defun org-blog--get-pre-processed-buffer ()
"Returns a pre-processed copy of the current buffer.
Internal links to other subtrees are converted to external
links."
(let* ((buffer (generate-new-buffer (concat "*Ox-hugo Pre-processed " (buffer-name) " *")))
(ast (org-element-parse-buffer))
(info (org-combine-plists
(list :parse-tree ast)
(org-export--get-export-attributes 'hugo)
(org-export--get-buffer-attributes)
(org-export-get-environment 'hugo)))
(local-variables (buffer-local-variables))
(bound-variables (org-export--list-bound-variables))
vars)
(with-current-buffer buffer
(let ((inhibit-modification-hooks t)
(org-mode-hook nil)
(org-inhibit-startup t))
(org-mode)
(dolist (entry local-variables vars)
(when (consp entry)
(let ((var (car entry))
(val (cdr entry)))
(and (not (memq var org-export-ignored-local-variables))
(or (memq var
'(default-directory
buffer-file-name
buffer-file-coding-system))
(assq var bound-variables)
(string-match "^\\(org-\\|orgtbl-\\)"
(symbol-name var)))
(or (not val) (ignore-errors (read (format "%S" val))))
(push (set (make-local-variable var) val) vars)))))
(org-element-map ast 'link
(lambda (link)
(let ((type (org-element-property :type link)))
(when (member type '("custom-id" "id" "fuzzy"))
(let* ((raw-link (org-element-property :raw-link link))
(destination (if (string= type "fuzzy")
(org-export-resolve-fuzzy-link link info)
(org-export-resolve-id-link link info)))
(source-path (org-blog--get-element-path link info)) (destination-path (org-blog--get-element-path destination info))
(destination-type (org-element-type destination)))
(unless (equal source-path destination-path)
(let ((link-desc (org-element-contents link))
(link-copy (org-element-copy link)))
(apply #'org-element-adopt-elements link-copy link-desc)
(org-element-put-property link-copy :type "file")
(org-element-put-property
link-copy :path
(cond
((org-element-property :EXPORT_FILE_NAME destination)
(concat destination-path ".org"))
((and (string= type "fuzzy")
(not (string-prefix-p "*" raw-link)))
(concat destination-path ".org"))
((string= type "custom-id")
(concat destination-path ".org::" raw-link))
(t
(let ((anchor (org-hugo--get-anchor destination info)))
(concat destination-path ".org::#" anchor)))))
(when (and (null link-desc)
(equal 'headline destination-type))
(let ((headline-title
(org-export-data-with-backend
(org-element-property :title destination) 'ascii info)))
(org-element-set-contents link-copy headline-title)))
(org-element-set-element link link-copy))))))))
(org-element-map ast 'special-block
(lambda (block)
(when (null (org-element-contents block))
(org-element-adopt-elements block ""))))
(insert (org-element-interpret-data ast))
(set-buffer-modified-p nil)))
buffer))
#+end_src
UUID to keep track of sections.
I tried not to, but this is unavoidable in the end.
#+begin_src elisp
(defun uuid ()
(md5 (format "%s%s%s%s%s%s%s%s%s"
(user-uid)
(emacs-pid)
(system-name)
(user-full-name)
(current-time)
(emacs-uptime)
(garbage-collect)
(random)
(recent-keys))))
#+end_src
#+begin_src elisp
(defcustom org-blog-regen-toc&tags t
"is the toc and tags updated upon saving ?
this can be long and should be turned off to speed things up"
:group 'org-blog
)
#+end_src
(setq org-blog-regen-toc&tags nil)
(setq org-blog-regen-toc&tags t)
#+results:
: org-blog-regen-toc&tags
#+begin_src elisp
(defun org-export-subtree-to-html (&optional async visible-only all-subtrees)
"Export the current subtree to a html post.
inspired by the ox-hugo exporter.
A non-nil optional argument ASYNC means the process should happen
asynchronously. The resulting file should be accessible through the
`org-export-stack' interface.
When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.
When optional argument ALL-SUBTREES is non-nil, print the
subtree-number being exported.
- If point is under a valid Hugo post subtree, export it, and
also return the exported file name.
- or, if point is not under a valid Hugo post subtree, but one exists
elsewhere in the Org file, do not export anything, but still
return t.
- Else, return nil."
(setq headline-list '()) (setq narrowed (buffer-narrowed-p))
(save-restriction
(save-excursion
(ignore-errors
(condition-case err
(outline-up-heading
(+ -1(nth 1 (org-heading-components))))
(error () nil)) (when (not narrowed)
(org-narrow-to-subtree)))
(let ((subtree (org-element-at-point)))
(if (not (member (nth 2 (org-heading-components)) org-done-keywords)) (progn
(widen)
(message "section is not in a DONE state (%s)" (org-heading-components)))
(let* ((info (org-combine-plists
(org-export--get-export-attributes
'blog subtree visible-only)
(org-export--get-buffer-attributes)
(org-export-get-environment 'blog subtree)))
(id (save-excursion (org-up-heading 1) (org-entry-get nil "ID")))
(export-file-name (save-excursion (org-up-heading 1)
(org-entry-get nil "EXPORT_FILE_NAME")))
(is-commented (org-element-property :commentedp subtree))
(dir default-directory)
(time-&-title (timestamp-and-title
(org-element-property :title subtree)))
(title (plist-get time-&-title :title))
(title-tags (org-entry-get nil "ALLTAGS" t))
(exclude-tags (plist-get info :exclude-tags)) (is-excluded (seq-intersection '(split-string (or title-tags "") ":")
'(exclude-tags)))
)
(cond
(is-commented
(message "[ox-weblog] `%s' was not exported as that subtree is commented"
title))
(is-excluded
(message "[ox-weblog] `%s' was not exported as it is tagged with an exclude tag `%s'"
title matched-exclude-tag))
(t
(if all-subtrees
(progn
(setq org-blog--subtree-count (1+ org-blog--subtree-count))
(message "[ox-weblog] %d/ exporting `%s' .." org-blog--subtree-count title))
(message "[ox-weblog] exporting `%s' .." title))
(when (not id)
(save-excursion
(setq id (uuid))
(message "insert new id %s" id)
(org-up-heading 1)
(org-entry-put nil "ID" id)
))
(let*((current-outline-path (org-get-outline-path :with-self))
(buffer (org-blog--get-pre-processed-buffer)) (sourcefile (f-filename (buffer-file-name))) (filename (or (org-entry-get nil "EXPORT_FILE_NAME" t)
(sluggify title)) )
(file.org (concat filename ".org"))
(org/file.org (concat"./org/" file.org))
(file.html (concat filename ".html"))
(article-title (concat org-weblog-title " :: " title)))
(message "htmlizing")
(cd dir)
(with-current-buffer (org-org-export-as-org)
(outline-show-all)
(if (eq t(compare-strings (concat filename ".org") nil nil
sourcefile nil nil))
(message
"[ox-blog] org post export ( source file (%s) and post title (%s) share
the same name. Would overwrite the former. Skipping."
sourcefile filename)
(write-file org/file.org))
(cd dir)
(with-current-buffer (htmlize-buffer) (rename-buffer article-title)
(write-file (concat org/file.org ".html") nil)
(kill-buffer)
)
(kill-buffer))
(with-current-buffer buffer
(org-up-heading 1)
(message "[ox-blog] exporting to html > %s.html" filename)
(setq ret
(org-export-to-file 'weblog file.html
async :subtreep visible-only nil
(list :title-tags title-tags :export-article t )))
(message "exported %s %s (%s)" filename title title-tags)
(cd dir)
(when org-blog-regen-toc&tags
(save-excursion
(blog-toc-insert-headline-and-summary
filename time-&-title title-tags id export-file-name)))
(message "killing buffer %s" file.org)
(message "subtree exported")
)
(message "killing buffer %s" buffer)
(kill-buffer buffer)
(other-window 1)
)))
ret))))))
#+end_src
*** Elementary styles
#+begin_src elisp
(setq org-html-text-markup-alist
'((bold . "<b>%s</b>")
(code . "<code class='code'>%s</code>")
(italic . "<i>%s</i>")
(strike-through . "<del>%s</del>")
(underline . "<u>%s</u>")
(verbatim . "<code class='verbatim'>%s</code>")))
#+end_src
** Parts from ox-hugo
*** save hook, export
#+begin_src elisp
(defun org-blog-export-to-html-after-save ()
"Function for `after-save-hook' to run `org-export-subtree-to-html'.
Need to check what goes on when Org Capture in progress."
(unless (eq real-this-command 'org-capture-finalize)
(save-excursion
(org-export-subtree-to-html))))
(define-minor-mode org-blog-auto-export-mode
"Toggle auto exporting the subtree upon saving"
:global nil
:lighter "autoexport"
:keymap org-mode-map
(if org-blog-auto-export-mode
(add-hook 'after-save-hook #'org-blog-export-to-html-after-save :append :local)
(remove-hook 'after-save-hook #'org-blog-export-to-html-after-save :local)))
(defun org-blog--get-valid-subtree ()
"Return the Org element for a valid blog post subtree.
The condition to check validity is that the STATE of the
entry is in a DONE state.
As this function is intended to be called inside a valid
post subtree, doing so also moves the point to the beginning of
the heading of that subtree.
Return nil if a valid post subtree is not found. The point
will be moved in this case too."
(if (member (nth 3 (org-element-at-point)) org-done-keywords)
(cl-return entry)
(cl-return nil)))
#+end_src
this is the entry point when in a headline to be exported.
And which can be hooked up to be saved.
[2021-06-02 mer. 00:39] got it working for the export to md.
It asks for the target filename.
- [X] shoud be auto set with the slug
- [X] remove the filename/target assoc
- [X] next, give it the derived html backend.
- [X] use the tags to build indexes/Table of contents (which is an org file)
(org-export-subtree-to-html) (profiler-report) (profiler-stop)
*** TODO Change the target/publishing directory
Here's how ox-hugo does:
#+begin_src elisp
(defun org-hugo--get-pub-dir (info)
"Return the post publication directory path.
The publication directory is created if it does not exist.
INFO is a plist used as a communication channel."
(let* ((base-dir (if (plist-get info :hugo-base-dir)
(file-name-as-directory (plist-get info :hugo-base-dir))
(user-error "It is mandatory to set the HUGO_BASE_DIR property")))
(content-dir "content/")
(section-path (org-hugo--get-section-path info))
(bundle-dir (let ((bundle-path (or (org-hugo--entry-get-concat nil "EXPORT_HUGO_BUNDLE" "/")
(plist-get info :hugo-bundle)))) (if bundle-path
(file-name-as-directory bundle-path)
"")))
(pub-dir (let ((dir (concat base-dir content-dir section-path bundle-dir)))
(make-directory dir :parents) dir)))
(file-truename pub-dir)))
#+end_src
#+begin_src elisp
(defun org-hugo-export-to-md (&optional async subtreep visible-only)
"Export current buffer to a Hugo-compatible Markdown file.
If narrowing is active in the current buffer, only export its
narrowed part.
If a region is active, export that region.
A non-nil optional argument ASYNC means the process should happen
asynchronously. The resulting file should be accessible through
the `org-export-stack' interface.
When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.
When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.
Return output file's name."
(interactive)
(org-hugo--before-export-function subtreep)
(let* ((org-use-property-inheritance (org-hugo--selective-property-inheritance))
(info (org-combine-plists
(org-export--get-export-attributes
'hugo subtreep visible-only)
(org-export--get-buffer-attributes)
(org-export-get-environment 'hugo subtreep)))
(pub-dir (org-hugo--get-pub-dir info))
(outfile (org-export-output-file-name ".md" subtreep pub-dir)))
(prog1
(org-export-to-file 'hugo outfile async subtreep visible-only)
(org-hugo--after-export-function info outfile))))
#+end_src
*** • auto-export notice
Add a highlight when autoexport: enabled ? is active
This doesn't work in comment
and won't override an existing formating (like a strike)
nor a comment
#+begin_src elisp
(defgroup org-web-blog nil
"Eye candy for Org weblog"
:tag "Org Web Blog"
:group 'org-appearance)
(defface blog-auto-export-enabled '((t :inherit default :foreground "chartreuse" :weight "bold"))
"Face used to mention auto export is enabled"
:group 'org-web-blog)
(defface greenout '((t :inherit default :foreground "chartreuse" :background "chartreuse"))
"Face blackout the ongoing interogation"
:group 'org-web-blog)
(add-hook 'org-blog-auto-export-mode-hook
(lambda ()
(font-lock-add-keywords nil
'(("\\<autoexport: \\(enabled\\)" 1 'blog-auto-export-enabled prepend)
("\\<enabled \\(\\?\\)" 1 'greenout prepend)
))
(font-lock-fontify-buffer)
))
#+end_src
** Tags and summary
*** First attempts
with org-elements.el.
There's some doc on worg
but really, the sources of org say everything.
**** The complex way
Reparse elements. They are cons with (type, properties, &rest elements)
#+begin_src emacs-lisp
(defun org-element-mapper (p)
(cl-case (org-element-type p)
('paragraph (mapconcat #'identity (mapcar #'org-element-mapper (org-element-contents p)) ""))
('bold (format "*%s*" (car (org-element-contents p))))
('italic (format "/%s/" (car (org-element-contents p))))
('verbatim (format "=%s= " (org-element-property :value p)))
('plain-text (format "%s" p))
('link (org-element-property :raw-link p))
('src-block (format "\n#+src\n%s" (org-element-contents p)))
(t
(format "<%s> >%s [%s] " (org-element-type p) (type-of p) p )))))
#+end_src
#+begin_src emacs-lisp :results raw
(org-element-map (org-element-parse-buffer) 'paragraph
(lambda (paragraph)
(let ((parent (org-element-property :parent paragraph)))
(and (eq (org-element-type parent) 'section)
(let ((first-child (car (org-element-contents parent))))
(eq first-child paragraph))
paragraph))))
#+end_src
#+begin_src emacs-lisp
(org-element-map (org-element-parse-buffer) 'paragraph
(lambda (paragraph)
(let ((parent (org-element-property :parent paragraph)))
(and (eq (org-element-type parent) 'section)
(let ((first-child (car (org-element-contents parent))))
(eq first-child paragraph))
(org-element-mapper paragraph)))))
#+end_src
**** other attempts
#+begin_src emacs-lisp :results raw
(org-element-map (org-element-parse-buffer ) 'paragraph
(lambda (paragraph)
(let ((parent (org-element-property :parent paragraph)))
(and (eq (org-element-type parent) 'section)
(let ((first-child (car (org-element-contents parent))))
(eq first-child paragraph))
(org-element-interpret-data paragraph)))))
#+end_src
#+begin_src emacs-lisp :results raw
(org-element-map
(org-element-parse-buffer 'object t)
#'(paragraph)
#'org-element-interpret-data
nil nil )
#+end_src
#+begin_src emacs-lisp :results raw
(save-excursion
(org-up-heading 1)
(org-narrow-to-subtree)
(let* ((tree (org-element-parse-buffer 'object t)))
(org-element-map tree '(paragraph)
(lambda (p) (org-element-contents p))
nil nil t )))
#+end_src
#+begin_src emacs-lisp :results raw
(save-excursion
(let* (
(tree (org-element-parse-buffer 'object t))
)
(org-element-map tree
'(paragraph bold)
(lambda (p) (car (org-element-contents p)))
nil nil nil t
))))
#+end_src
**** The easy way : org-interpret data
Getting only the paragraph
#+begin_src emacs-lisp :results raw replace
(save-excursion
(mapconcat #'identity
(org-element-map
(org-element-parse-buffer 'object t)
'(paragraph)
'org-element-interpret-data
nil nil )
" "))
#+end_src
** Summary
#+begin_src elisp
(defgroup org-weblog nil "the not so easy org blog" :version 26.1)
(defcustom org-blog-words-in-summary
100 "maximum number of words in a blog entry summary" :group 'org-weblog)
#+end_src
Gettings visible paragraphs
#+begin_src emacs-lisp
(mapconcat #'identity (org-element-map (org-element-parse-buffer 'object t)
'(paragraph)
#'org-element-interpret-data
nil nil ) " ")
#+end_src
Take the begining of the text following the headline
to make a summary displayed in ./index.org.
#+begin_src elisp
(defun subtree-summary ()
(save-excursion
(save-restriction
(org-up-heading 1)
(org-narrow-to-subtree)
(org-show-all)
(setq blog-org-structure
(org-element-map
(org-element-parse-buffer 'object t)
'paragraph #'org-element-interpret-data))
(or (org-entry-get nil "SUMMARY" t)
(with-temp-buffer (insert
(mapconcat #'identity
blog-org-structure
" "))
(goto-char (point-min))
(setq num 0)
(while (and
(< num org-blog-words-in-summary)
(not (eq "summarystop" (word-at-point)))) (forward-word) (setq num (+ 1 num)))
(concat (buffer-substring-no-properties (point-min) (point)) "...")))
)))
#+end_src
Young and unaware programmer as I once was, I did code my own string
library in C++. I met many bugs while doing (double frees among other
niceties...) learned the usage of operators and put them everywhere.
While coding it, I realized that a high level programming language or
a clever compiler would make it possible to create an editor able
to behave equally in an interactive mode and in a program: functions
behaviour could be tested interactively on samples; strings
operators and memory buffers would need to be 100% correct.
** ToC of articles: links and summaries
We don't create the index.org file,
there should already be one (for now)
with the following structure, so that
org-insert-heading-after-current works.
That's mostly because I'm going fast on that section
index.org
#+begin_src org
,#+TITLE: 7d.nz
,#+EMAIL: pe@7d.nz
,#+RSS_FEED_URL: https://7d.nz
#+end_src
./index.org is the default website's landing page
Open index.org and run (org-export-to-file 'weblog "index.html")
to change the ToC.
#+begin_src elisp
(defcustom org-blog-toc-entry-file "index"
"the index file holding the main ToC entry with all articles titles and summary" :group 'blaxorg)
#+end_src
and perhaps later paginated
#+begin_src elisp
(defun blog-toc-insert-headline-and-summary
(filename ttitle tags id export-filename)
"add a new entry in index.org"
(message "updating %s :: %s" org-blog-toc-entry-file ".org" id)
(let((summary (subtree-summary))
(current (current-buffer))
(index·org (concat org-blog-toc-entry-file ".org"))
(index·html (concat org-blog-toc-entry-file ".html"))
)
(with-current-buffer(find-file index·org)
(goto-char (point-min))
(while (and (outline-next-heading)
(if (equal (org-entry-get nil "ID") id)
(progn (org-cut-subtree) nil)
t)))
(org-insert-heading) (insert (concat
(plist-get ttitle :title) " " tags "\n"
summary
"\n"
))
(org-entry-put nil "ID" id) (when export-filename
(org-entry-put nil "EXPORT_FILE_NAME" export-filename))
(org-entry-put nil "DATE" (plist-get ttitle :timestamp))
(mark-whole-buffer) (org-sort-entries nil ?R nil nil "DATE") (deactivate-mark)
(org-export-to-file 'weblog index·html) (write-file index·org)
(when org-blog-regen-tags-p (org-blog-export-toc-tags))
(org-delete-keyword "TITLE") (org-insert-keyword "TITLE" " ")
)
(if (get-buffer index·org) (with-current-buffer index·org (revert-buffer t t)))))
#+end_src
#+begin_src example
(blog-toc-insert-headline-and-summary
"test.org"
(timestamp-and-title (nth 4(heading-components-at-level 1)))
(nth 5 (heading-components-at-level 1)) 0 "ex.org")
#+end_src
** ox-rss
#+begin_src emacs-lisp
(use-package ox-rss)
(let(
(index·org (concat org-blog-toc-entry-file ".org"))
)
(with-current-buffer(find-file index·org)
(org-delete-keyword "TITLE")
(org-insert-keyword "TITLE" org-weblog-title)
(org-rss-export-to-rss)
(org-delete-keyword "TITLE")
(org-insert-keyword "TITLE" " ")))
#+end_src
or C-e r r from index.org.
** Look for an entry with a given property (id)
There is only one entry to a given article in the index. In a
sophisticated solution, those files are linked in a merkel tree by
consecutive revision. Only one sha1 would then be required to retrieve
an entire branch and history, but this is not git nor venti.
#+begin_src emacs-lisp
(while (and (outline-next-heading)
(if (equal (org-entry-get nil "ID") id)
(progn (org-cut-subtree) nil)
t)))
#+end_src
** To sort main ToC entries by time
#+begin_src emacs-lisp
(mark-whole-buffer)
(org-sort-entries nil ?T) #+end_src
** To prevent duplicates
When exporting a section and before writing the org file, the export
is triggered by title or body change. a title change also implies, if
there's no EXPORT_FILE_NAME, a filename change but the text body might
have stayed the same. In wich case, we ought to simply rename the
file.
Grepping for :ID: will also point duplicates.
The following is an idea to identify and book-keep exports.
check the body sha1 isn't found in the dir.sig file :
the body is the post section past the first headline.
If no such file exists, create it and append
the sha1 of the text-body followed by filename
Register it in a dir.sig file.
This is where to look for :
- filenames. They're unique as they are in the same directory.
- sha1 of text body. unique.
If one or the other is found twice, adjust accordingly.
- body changes : proceed, update the sha1
- title changes: move filename, update title.
If there's only an EXPORT_FILE_NAME change,
this is considering as both a change in body and title.
If there are both changes in body and title
then we can no longer keep track of the change.
We could still introduce a section Id in the properties,
but I'm trying to avoid to clutter the sources with
extra data. The sha1 may go in the listing and summary.
Here is a good place to mention we could map org-data
onto something more racidal : folders.
#+begin_src elisp
(defun bookkeep-export-buffer()
(goto-line 2)
(sha1 buffer (point) (point-max)))
#+end_src
In the end, it's a far more complicated
solution than preserving an ID property.
Let's move on.
** Tags
*** list of tags in file
#+begin_src elisp
(defun org-buffer-tags()
(let (tags)
(org-with-point-at (point-min)
(while (outline-next-heading)
(let ((curnt-tags (nth 5 (org-heading-components))))
(when curnt-tags
(mapcar (lambda (v) (setq tags (append tags (list v))))
(org-split-string curnt-tags ":"))))))
(seq-uniq tags)))
#+end_src
(org-buffer-tags)
but there's already (org-get-buffer-tags).
(mapcar 'car (org-get-buffer-tags)).
*** Org export to org
#+begin_src elisp
(defun blog-export-to-org-tag(tag)
"export only selected tag as <tag>.org
good care is advised with tags and filenames if they're not in their own folders"
(setq org-export-select-tags '(tag))
(org-org-export-to-org)
(setq org-export-select-tags '("export"))) #+end_src
Need to set an export file name,
so look for it, remove it if there's one
and set that one instead.
The cursor mark can stays after colon.
#+begin_example
,#+EXPORT_FILE_NAME: dev.org
#+end_example
Then Call org-kill-line, insert new name, continue.
To get the export file name:
/usr/local/share/emacs/lisp/org/ox.el:6417
#+begin_src emacs-lisp
(org-with-point-at (point-min)
(catch :found
(let ((case-fold-search t))
(while (re-search-forward
"^[ \t]*#\\+EXPORT_FILE_NAME:[ \t]+\\S-" nil t)
(let ((element (org-element-at-point)))
(when (eq 'keyword (org-element-type element))
(throw :found
(org-element-property :value element))))))))
#+end_src
From where I derived this generic function
#+begin_src elisp
(defun org-search-keyword(keyword)
(org-with-point-at (point-min)
(catch :found
(let ((case-fold-search t))
(while (re-search-forward
(concat "^[ \t]*#\\+" keyword ":[ \t]+\\S-")
nil t)
(let ((element (org-element-at-point)))
(when (eq 'keyword (org-element-type element))
(throw :found
(org-element-property :value element)))))))))
#+end_src
: (org-search-keyword "EXPORT_FILE_NAME")
: (org-insert-directive "EXPORT_FILE_NAME" "/tmp/t.org")
They are called #+keywords, but I think they should would better be named directives.
#+begin_src elisp
(defun org-insert-keyword(keyword value)
(org-with-point-at (point-min)
(insert (concat "#+" keyword ": " value "\n"))))
#+end_src
#+begin_src elisp
(defun org-delete-keyword(keyword)
"kill a line with a keyword"
(org-with-point-at (point-min)
(when (org-search-keyword keyword)
(goto-char(point-min))
(re-search-forward
(concat "^[ \t]*#\\+" keyword ":[ \t]+")
nil t)
(org-beginning-of-line)
(org-kill-line))))
#+end_src
Problem here: I may need to derive the backend
for headlines with EXPORT_FILE_NAME property set
to correcly link the appropriate file.
Or I can completly ignore this feature and move on.
#+begin_src emacs-lisp
(defun org-export-all-tags()
(org-delete-keyword "EXPORT_FILE_NAME")
(org-insert-keyword "EXPORT_FILE_NAME" "")
(mapcar (lambda (tag)
((save-excursion
(insert tag ".org") (org-export-to-org-tag tag))
(org-kill-line))
(org-buffer-tags))
(org-delete-keyword "EXPORT_FILE_NAME")))
#+end_src
#+begin_src elisp
(defun org-blog-export-toc-tags()
"for all tags at headline level 1
produce one .org and one .html file with headlines and summaries"
(message "generating tags files")
(org-delete-keyword "EXPORT_FILE_NAME")
(org-with-point-at (point-min)
(insert (concat "#+EXPORT_FILE_NAME: \n"))
(left-char)
(mapcar (lambda (tag)
(let ((org-file (concat tag ".org"))
(html-file (concat tag ".html")))
(save-excursion
(org-kill-line)
(insert org-file) (setq org-export-select-tags (list tag))
(org-delete-keyword "TITLE")
(org-insert-keyword "TITLE" tag)
(org-export-to-file 'weblog html-file) (org-kill-line) )))
(org-buffer-tags))
(setq org-export-select-tags '("export"))))
#+end_src
** Language identifier mapping
The Syntax Highlighter uses sometimes other language identifiers for
source blocks than org-mode. For example, where org-mode uses sh,
Syntax Highlighter uses bash.
The mappings goes in an alist for later use.
#+begin_src emacs-lisp
(defconst ox-blog-language-terms
("emacs-lisp" . "lisp")
("elisp" . "lisp")
("sh" . "bash")))
#+end_src
** Exporting source code blocks
The source code exporting function, ox-blog-src-block, is
modelled on org-html-src-block in org-mode/lisp/ox-html.el.
To keep things simple, the caption and label code is deleted. The
language identifier is mapped using the mapping defined in the alist
above. The HTML-formatting of the source code is removed, instead of
org-html-format-code we use org-export-unravel-code. At last the
pre formatting in angles is changed to code in brackets.
#+begin_src elisp
(defun ox-blog-src-block (src-block contents info)
"Transcode a SRC-BLOCK element from Org to HTML.
CONTENTS holds the contents of the item. INFO is a plist holding
contextual information."
(if (org-export-read-attribute :attr_html src-block :textarea)
(org-html--textarea-block src-block)
(let ((language-term
(or (cdr (assoc (org-element-property :language src-block)
ox-blog-language-terms))
(org-element-property :language src-block)))
(code (car (org-export-unravel-code src-block))))
(if (not language-term)
(format "<pre class=\"example\"%s>\n%s</pre>" label code)
(format "<div class=\"org-src-container\">\n%s\n</div>"
(format "\n[code lang=\"%s\"]%s[/code]" language-term code))))))
#+end_src
** Wrapping up and autoload
The following goes in to file:./.dir-locals.el
#+begin_src emacs-lisp :tangle .dir-locals.el
((nil . ((indent-tabs-mode . nil)
(fill-column . 70)
(sentence-end-double-space . t)))
("org-sources" . ((org-mode . ( (eval . (org-blog-auto-export-mode)))))))
#+end_src
This will start the org-blog-auto-export,
loading the required libraries when opening
files in the directory.
** Less verbose message
https://scripter.co/using-emacs-advice-to-silence-messages-from-functions/
#+begin_src elisp
(defun org-hugo--advice-silence-messages (orig-fun &rest args)
"Advice function that silences all messages in ORIG-FUN."
(let ((inhibit-message t) (message-log-max nil)) (apply orig-fun args)))
#+end_src
or multiple function :
#+begin_src emacs-lisp
(dolist (fn '(org-babel-exp-src-block write-region)
(advice-add fn :around #'org-hugo--advice-silence-messages))
#+end_src
#+begin_src elisp
#+end_src
** Possible improvements
Interactivity could be improved by activating
impatient mode.
The publishing process isn't long.
but since org-html-export-to-html can run asynchronously,
the async option could initiate a new emacs process,
which doesn't need to load your working init.el
and freeze the running session.
So, we'll go into starting emacs with an alternate init
file and deal about process management in a next chapter.
: emacs -q -l export.el ...
+argument ? -eval expression?
How does command line works in Emacs by the way?
Do we really need an asynchronous process?
What does ob-comint asyncs does?
Did you enjoy the reading?
Thank you, have a nice day and come back later for more.