/ox-tailwind

Org-Mode HTML export back-end with Tailwind.css classes. Check the theme:

Primary LanguageCSSMIT LicenseMIT

Ox-Tailwind Export Back-End

Check the output in ox-tailwind GitHub docs.

This back-end has the purpose of allowing easy customization of the HTML output. Although it is called Tailwind, the only thing that it does is allowing you to customize the classes of the HTML and exporting a more barebones HTML (It does not create as many divs and sections as the normal HTML export back-end). Instead of using Tailwind.css you can just name the classes of the elements and import your own css (or edit ./css/style.css).

Note: Although you can export any .org file as html by using C-c C-e x h (through org-export-dispatcher), this method won’t create the file inside the dist folder where you have all the required js and css files. If you open this file on a browser and open the console you will notice there are import errors. You should instead setup your notes directory with org-publish-project-alist and then publish the project using (org-publish-all) or (org-publish-all t) to forcefully publish files.

Installation

To install this back-end, clone this repo into your packages or private folder in your ~~/.emacs.d~ directory:

git clone https://github.com/vascoferreira25/ox-tailwind
(out)Cloning into '~/.emacs.d/private'...
(out)remote: Enumerating objects: 49, done.
(out)remote: Counting objects: 100% (49/49), done.
(out)remote: Compressing objects: 100% (40/40), done.
(out)remote: Total 49 (delta 20), reused 28 (delta 8), pack-reused 0
(out)Unpacking objects: 100% (49/49), done.

And add the following to your config file:

;; ox-tailwind export back-end
(load "~/.emacs.d/private-packages/ox-tailwind/ox-tailwind.el")

Afterwards, copy the notes_example folder to get an already made directory structure for your notes including all the required CSS and JS files. You just have to setup org-publish-project-alist and then use the (org-publish-all) command and you are done. Check the /notes_example/index.org file.

Straight Package Management

If you use straight as your package management, add this to your config file:

(straight-use-package
 '(ox-tailwind :type git :host github :repo "vascoferreira25/ox-tailwind"))

Doom

To use this package with Doom Emacs, add the package to packages.el:

(package! ox-tailwind
  :recipe (:host github :repo "vascoferreira25/ox-tailwind"))

And then, load it after ox-html

(after! ox-html (require 'ox-tailwind))

Org-Roam

If you use org-roam and want to show the backlinks of the current file, add this function to your config:

;; Add backlinks to the export
(defun collect-backlinks-string (backend)
  (when (org-roam-node-at-point)
    (let* ((source-node (org-roam-node-at-point))
           (source-file (org-roam-node-file source-node))
           ;; Sort the nodes by the point to avoid errors when inserting the
           ;; references
           (nodes-in-file (--sort (< (org-roam-node-point it)
                                     (org-roam-node-point other))
                                  (-filter (lambda (node)
                                             (s-equals?
                                              (org-roam-node-file node)
                                              source-file))
                                           (org-roam-node-list))))
           ;; Nodes don't store the last position so, get the next node position
           ;; and subtract one character
           (nodes-start-position (-map (lambda (node) (org-roam-node-point node))
                                       nodes-in-file))
           (nodes-end-position (-concat (-map (lambda (next-node-position)
                                                (- next-node-position 1))
                                              (-drop 1 nodes-start-position))
                                        (list (point-max))))
           ;; Keep track of the current-node index
           (current-node 0)
           ;; Keep track of the amount of text added
           (character-count 0))
      (when (and nodes-in-file (s-equals? backend "latex"))
        (insert "\n\n\\clearpage\n\n"))
      (dolist (node nodes-in-file)
        (when (org-roam-backlinks-get node)
          ;; Go to the end of the node and don't forget about previously inserted
          ;; text
          (goto-char (+ (nth current-node nodes-end-position) character-count))
          ;; Add the references as a subtree of the node
          (setq heading (format "\n\n%s Backlinks\n"
                                (s-repeat (+ (org-roam-node-level node) 1) "*")))
          ;; Count the characters and count the new lines (4)
          (setq character-count (+ 3 character-count (string-width heading)))
          (insert heading)
          ;; Insert properties drawer
          (setq properties-drawer ":PROPERTIES:\n:HTML_CONTAINER_CLASS: references\n:END:\n")
          ;; Count the characters and count the new lines (3)
          (setq character-count (+ 3 character-count (string-width properties-drawer)))
          (insert properties-drawer)
          (dolist (backlink (org-roam-backlinks-get node))
            (let* ((source-node (org-roam-backlink-source-node backlink))
                   (point (org-roam-backlink-point backlink))
                   (text (s-replace "\n" " " (org-roam-preview-get-contents
                                              (org-roam-node-file source-node)
                                              point)))
                   (references (format "- [[./%s][%s]]: %s\n\n"
                                       (file-relative-name (org-roam-node-file source-node))
                                       (org-roam-node-title source-node)
                                       text)))
              ;; Also count the new lines (2)
              (setq character-count (+ 2 character-count (string-width references)))
              (insert references))))
        (setq current-node (+ current-node 1))))))

(add-hook 'org-export-before-processing-hook 'collect-backlinks-string)

Notes structure

In order for the search bar to work, this export backend will search all .org files in the path of your project and store all the headings in a .js file.

The folder structure for your notes should be like this:

tree /F /a
(out)C:/path/to/your_notes/
(out)|   index.org
(out)|   ... other org files
(out)|   
(out)+---files
(out)|       # Your attachments
(out)|
(out)+---img
(out)|       # Your image files
(out)|       
(out)+---dist
(out)        # Org Publish Files

The home page for your notes should be index.org.

For easier search and note management, the notes should be named as <subject>_<notes_name>_<guide|snippets|templates|...>.org.

Examples:

NotesFile Name
Programming Guides Indexprogramming.org
Python notes Indexprogramming_python.org
Python guideprogramming_python_guide.org
Python snippetsprogramming_python_snippets.org
Guides Indexguides.org
How to manage Ebooks guideguides_manage_ebooks.org
Gaming Indexgaming.org
Skyrim guidegaming_skyrim.org
Subjects Indexsubjects.org
Mathematicssubjects_mathematics.org
Economicssubjects_economics.org

Index files should have the following template

* Subjects
** Pages
   
[[./subjects_accounting_and_finance.org][Accounting and Finance]]

[[./subjects_computer_science.org][Computer Science]]

[[./subjects_economics.org][Economics]]

[[./subjects_elo_rating.org][Elo Rating System]]

[[./subjects_mathematics.org][Mathematics]]

[[./subjects_statistics.org][Statistics]]

** References

Publish settings

To setup automatic export of all my org files I use the following settings:

(setq org-publish-project-alist
      '(("org-files"
         :base-extension "org"
         :base-directory "V:/orgmode/"
         :publishing-directory "V:/orgmode/dist/"
         ;; or use `org-tailwind-publish-to-html' to generate the toc after each
         ;; file - *note*: it will be slower to parse the whole project
         :publishing-function org-tailwind-publish-to-html-without-toc) 
        ("images"
         :base-directory "V:/orgmode/img/"
         :base-extension ".*"
         :publishing-directory "V:/orgmode/dist/img/"
         :publishing-function org-publish-attachment)
        ("files"
         :base-directory "V:/orgmode/files/"
         :base-extension ".*"
         :publishing-directory "V:/orgmode/dist/files/"
         :publishing-function org-publish-attachment)
        ("tangles"
         :base-directory "V:/orgmode/tangles/"
         :base-extension ".*"
         :publishing-directory "V:/orgmode/dist/tangles/"
         :publishing-function org-publish-attachment)
        ;; Publish all in one time
        ("notes" :components ("org-files" "images" "files" "tangles"))))

After setting up your notes path, you should use (org-publish-all) to publish all the notes as html.

**Note**: Before publishing, open a buffer on one of your .org files or just dired into the notes directory. As this back-end needs to create a .js file based on your .org files to enable searching, if the Emacs current directory isn’t in the notes directory, it will fail to create this file.

In order to be faster to parse all your notes, it is advisable to only generate the toc file after publishing. Use the following functions instead of the org-export-dispatch to automatically generate the toc after publishing:

(defun publish-file-and-build-toc ()
  "Force publish the current org-mode file."
  (interactive)
  (org-publish-current-file)
  (org-tailwind-build-toc))

(defun force-publish-file-and-build-toc ()
  "Force publish the current org-mode file."
  (interactive)
  (org-publish-current-file t)
  (org-tailwind-build-toc))

(defun publish-all-and-build-toc ()
  "Force publish all org-mode files."
  (interactive)
  (org-publish-all)
  (org-tailwind-build-toc))

(defun force-publish-all-and-build-toc ()
  "Force publish all org-mode files."
  (interactive)
  (org-publish-all t)
  (org-tailwind-build-toc))

Notes output directory

In order for the export to work, you need to put the required files in the output folder. Just copy the /notes_example/dist folder into your notes /dist/ folder.

This is the directory structure of the export folder:

tree /F /a
(out)C:/path/to/your_notes/dist/
(out)|   # The HTML export
(out)|   index.html
(out)|   
(out)+---css
(out)|       prism.css
(out)|       style.css # Your css file
(out)|       tailwind.min.css
(out)|       
(out)+---files
(out)|       # Your attachments
(out)+---img
(out)|       # Your image files
(out)|       spacemacs_1.png
(out)|       spacemacs_2.png
(out)|       
(out)+---js
(out)|       clipboard.min.js
(out)|       mermaid.min.js
(out)|       polyfill.min.js
(out)|       prism.js
(out)|       tex-mml-chtml.js
(out)|       toc_tree.js
(out)|       
(out)+---mathjax
(out)        # Mathjax Files

Cleaning the output folder

When publishing your org files, Org-Mode won’t delete any files in the /dist/ folder. If you delete org files and don’t delete those files from the /dist/ folder, you will end up with obsolete html files. In this case, what you should is delete all the html files and then use (org-publish-all t) to force publish all your org files again.

Also, if you delete images, tangles or other files from /your_notes/files, /your_notes/tangles or /your_notes/img there will be a copy of them in the /dist/ folder.

To completely clean the /dist/ folder you can delete all the following files and folders:

  • /dist/files,
  • /dist/img,
  • /dist/tangles,
  • all .html files.

Customization

Link in the top

By default, the file-name on the top of the page is a link for org-protocol.

You can customize the link by changing the variable org-tailwind-file-name-link or you can disable the link by changing the variable org-tailwind-file-name-use-link to nil.

Org-protocol settings

Setup the org-protocol.

(server-start)
(require 'org-protocol)

;; Custom protocols
(defun my-open-file-protocol-handler (data)
  (let* ((file-link (plist-get data :file))
         (file-path (replace-regexp-in-string "^\\(.*\\)#.*" "\\1" file-link)))
    (message "file-path: %s" file-path)
    (find-file file-path))
  ;; it must end in nil or it will create a buffer 
  nil)

(setq org-protocol-protocol-alist
      '(("open-file"
         :protocol "open-file"
         :function my-open-file-protocol-handler)))

Define a new link type for protocols and the export method:

(require 'ol)

(org-link-set-parameters "org-protocol"
                         :follow #'my-org-protocol-open
                         :export #'my-org-protocol-export
                         :store #'my-org-protocol-store-link)

(defun my-org-protocol-open (path _)
  "Visit the org-protocol on PATH.
PATH should be a topic that can be thrown at the man command."
  (browse-url path))

(defun my-org-protocol-store-link ()
  "Store a link to a man page."
  (when (memq major-mode '(org-mode))
    ;; This is a man page, we do make this link.
    (let* ((file (my-org-protocol-get-file-name))
           (link (concat "" file))
           (description (format "org-protocol for %s" file)))
      (org-link-store-props
       :type "org-protocol"
       :link link
       :description description))))

(defun my-org-protocol-get-page-name ()
  "Extract the file name from the buffer name."
  (if (string-match " \\(\\S-+\\)\\*" (buffer-name))
      (match-string 1 (buffer-name))
    (error "Cannot create link to this org-protocol file")))

(defun my-org-protocol-export (link description format _)
  "Export a man page link from Org files."
  (let ((path (format "org-protocol:%s" link))
        (desc (or description link)))
    (pcase format
      (`html (format "<a href=\"%s\">%s</a>" path desc))
      (`latex (format "\\href{%s}{%s}" path desc))
      (`texinfo (format "@uref{%s,%s}" path desc))
      (`ascii (format "%s (%s)" desc path))
      (t path))))

Export links to files with a link to open those files in Emacs through the protocol:

(defun my-open-in-emacs-link (backend)
  "Add a link to use org-protocol to open a file in emacs."
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward "\\(\\[\\[file:.*?\\]\\[.*\\]\\]\\)" nil t)
      (message "MATCHED: %s" (match-string-no-properties 1))
      (let* ((file-path (replace-regexp-in-string
                         ".*\\[\\[file:\\(.*?\\)\\]\\[.*"
                         "\\1"
                         (match-string-no-properties 1)))
             (fixed-file-path (replace-regexp-in-string
                               "\\(.*?\\)\\(::.*\\)?"
                               "\\1"
                               file-path)))
        (message "FILE_PATH: %s" fixed-file-path)
        (replace-match
         (format "%s ([[org-protocol://open-file?file=%s][open in Emacs]])"
                 (match-string-no-properties 1)
                 fixed-file-path))))))

(add-hook 'org-export-before-processing-hook 'my-open-in-emacs-link)

Customize Theme

To customize the theme you have to change the org-tailwind-class-... variables. There are multiple classes for all the Html tags. For example, changing the theme of a h1 tag:

(defcustom org-tailwind-class-h1
  "mt-24 mb-6 text-3xl text-gray-700 dark:text-gray-400 border-b \
hover:text-blue-400 dark:hover:text-blue-500 border-gray-500")

You can check all the other Html elements in the ox-tailwind.el file.

TailwindCSS

If you want to change TailwindCSS settings you can use the TailwindCSS - CLI tool. In the folder tailwind you can change the tailwind.config.js and build the css file. Then you just need to copy the output file to the /dist/css folder.

To create a config file you need to run:

cd ./tailwindcss
npx tailwindcss-cli@latest init

To build the css file run:

npx tailwindcss-cli@latest build -o ./tailwindcss/tailwind.css

Prism.js

To customize the code blocks, you can just download another theme from the Prism.js website and save both the js and the css file in your /dist folder. The link already has all the options selected, just select the theme you want and download both the js and css files and save them into the dist folder.

These are the required modules for Prism to work:

  • line highlight
  • line numbers
  • autolinker
  • file highlight
  • show language
  • jsonp highlight
  • inline color
  • previewers
  • autoloader
  • keep markup
  • command-line
  • unescaped markup
  • normalize whitespace
  • data-uri highlight
  • toolbar
  • copy to clipboard button
  • download button
  • match braces
  • diff highlight
  • filter highlight all
  • treeview

Mathjax

Mathjax has been downloaded from source by running:

git clone https://github.com/mathjax/MathJax.git mathjax

And then copy the files from /mathjax/es5 into the /dist/mathjax folder.

Mermaid.js

Mermaid has been downloaded from source by running:

git clone https://github.com/mermaid-js/mermaid.git

And then copy the files from /mermaid/dist into the /dist/js folder.

Elements

Markup

Text

Bold Text

Italic Text

Underlined Text

Strike Through

Verbatim

Inline code

HyperLinks

Lists

Ordered List

  1. Item number 1
    1. Item number 1.1
    2. Item number 1.2
    3. Item number 1.3
  2. Item number 2
  3. Item number 3
  4. Item number 4
  5. Item number 5

Unordered List

  • Like
    • This
      • One

Description List

Tip Blocks
Are for displaying tips.
Warning Blocks
Are for displaying warnings.
Danger Blocks
Are for displaying dangers.

Checkboxes

  • [ ] Unchecked 1
  • [ ] Unchecked 2
  • [X] Checked 1

Tables

ABC
<l><c><r>
In this columnIn thisFinally,
the textcolumnin this one
is left alignedit is centeredit is right aligned

Images

./files/weird_cat.gif

Videos

./files/cat_treats.mp4

Formulas

Inline formulas: $∑i=0^n i^2 = \frac{(n^2+n)(2n+1)}{6}$

$$∑i=0^n i^2 = \frac{(n^2+n)(2n+1)}{6}$$

Blocks

Blockquote

Once upon a time..........

Source Blocks

Source code blocks can be downloaded directly from github:

This uses the following attributes:

#+ATTR_FILENAME: core.cljs
#+ATTR_HIGHLIGHT: 2,6-8,11-20,48-51
#+ATTR_FETCH: https://api.github.com/repos/vascoferreira25/discord-bot/contents/src/main/core.cljs

Custom Blocks

There are four custom blocks: details, tip, warning and danger and these blocks can contain other elements. In order to get syntax highlighting while editing in Emacs, use org as language.

Details

Tip

Warning

Danger

Cool! Isn’t it?

Mermaids

There are also mermaids.

Diagram

Gantt Chart

Custom Attributes

The following blocks have custom attributes that you can change:

Source code
#+ATTR_HIGHLIGHT
lines to highlight in the source code, e.g. 1,5-10,12
#+ATTR_USERNAME
username to show in command-line blocks, e.g. CrazyCat
#+ATTR_HOSTNAME
hostname to show in command-line blocks, e.g. localhost
#+ATTR_FETCH
fetch files from the Github API
#+ATTR_FILEPATH
get files and add a download button, it uses HTTP so, no local files.
#+ATTR_FILENAME
name to display on the source code window.
Custom blocks
#+NAME
the title of the block
Tables
#+NAME
the description of the table
Images
#+NAME
the description of the image
Videos
#+NAME
the description of the video
#+ATTR_TIMELINE
the time of the start and/or end of the video, for example:
  • #+ATTR_TIMELINE: 5
  • #+ATTR_TIMELINE: 5,9
Blockquotes
#+NAME
the name of the author

Known bugs

  • It crashes when it encounters a line that ends in \\ - it works if it is inside a block;
  • It won’t export TODO keywords and SCHEDULE dates.