;;; one.el --- Static Site Generator for org-mode users -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2021-2022 Tony Aldon

;; Author: Tony Aldon <tony@tonyaldon.com>
;; Version: 1.1
;; Package-Requires: ((emacs "28.1") (jack "1.0") (htmlize "1.57"))
;; Keywords: hypermedia, outlines
;; Homepage: https://github.com/tonyaldon/one.el

;; 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.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; You can find the documentation at https://one.tonyaldon.com.
;;
;; In `one.el', the following org document defines a website with 3 pages
;; that we build by calling `one-build' command while we are visiting it:
;;
;; ---------------------------------
;; * My website
;; :PROPERTIES:
;; :ONE: one-default-home
;; :CUSTOM_ID: /
;; :END:
;;
;; Welcome to my website!
;;
;; * Blog post 1
;; :PROPERTIES:
;; :ONE: one-default
;; :CUSTOM_ID: /blog/page-1/
;; :END:
;;
;; My first blog post!
;;
;; * Blog post 2
;; :PROPERTIES:
;; :ONE: one-default
;; :CUSTOM_ID: /blog/page-2/
;; :END:
;;
;; My second blog post!
;; ---------------------------------
;;
;; Note that if we want to use the default css style sheet we can add it
;; by calling `one-default-add-css-file' before building the website.
;;
;; The path `/' in the first `CUSTOM_ID' org property tells `one.el' that the
;; page "My website" is the home page.  That page is rendered using
;; `one-default-home' render function, value of `ONE' org property of the
;; same headline.
;;
;; The path `/blog/page-1/' in the second `CUSTOM_ID' org property tells
;; `one.el' that we want to render "Blog post 1" page in such a way
;; that when we serve our website locally at `http://localhost:3000' for
;; instance, that page is served at `http://localhost:3000/blog/page-1/'.
;; How that page is rendered is determined by the value of `ONE' org
;; property of the same headline which is `one-default', a render
;; function.
;;
;; The same goes for the last page "Blog post 2".
;;
;; As you might have noticed, a `one.el' website is an org file where the
;; pages are the headlines of level 1 with the org properties `ONE' and
;; `CUSTOM_ID' set.  Nothing more!
;;
;; `ONE' is the only org property added by `one.el'.  Its value, an Emacs Lisp
;; function which returns an HTML string, for a given page determines how
;; `one.el' renders that page.
;;
;; Paths of pages are set using `CUSTOM_ID' org property.


;;; Code:

(require 'jack)
(require 'ox)
(require 'htmlize)

(defvar htmlize-buffer-places)

;;; utils

(defun one-escape (s)
  "Return the string S with some caracters escaped.
`<', `>' and `&' are escaped."
  (replace-regexp-in-string
   "\\(<\\)\\|\\(>\\)\\|\\(&\\)\\|\\(\"\\)\\|\\('\\)"
   (lambda (m) (pcase m
                 ("<"  "&lt;")
                 (">"  "&gt;")
                 ("&"  "&amp;")
                 ("\"" "&quot;")
                 ("'"  "&apos;")))
   s))

;;; one-ox
;;;; one backend

(org-export-define-backend 'one-ox
  '((headline . one-ox-headline)
    (section . one-ox-section)
    (paragraph . one-ox-paragraph)

    (plain-text . one-ox-plain-text)

    (bold . one-ox-bold)
    (italic . one-ox-italic)
    (strike-through . one-ox-strike-through)
    (underline . one-ox-underline)
    (code . one-ox-code)
    (verbatim . one-ox-verbatim)

    (subscript . one-ox-no-subscript)
    (superscript . one-ox-no-superscript)

    (plain-list . one-ox-plain-list)
    (item . one-ox-item)

    (src-block . one-ox-src-block)
    (example-block . one-ox-example-block)
    (fixed-width . one-ox-fixed-width)
    (quote-block . one-ox-quote-block)

    (table . one-ox-table)
    (table-row . one-ox-table-row)
    (table-cell . one-ox-table-cell)

    (link . one-ox-link)))

;;;; headline, section, paragraph, etc.

(defun one-ox-headline (headline contents _info)
  "Transcode a HEADLINE element from Org to HTML.

CONTENTS holds the contents of the headline.

Note that markups and links are not exported if used in headlines,
only the raw value string.  So don't use them in headlines."

  (let* ((level (org-element-property :level headline))
         (title (org-element-property :raw-value headline))
         ;; the property `:one-internal-id' is set by
         ;; `one-parse-buffer' This allow to produce unified
         ;; ids that can be use to build a TOC for each page.
         (id (org-element-property :one-internal-id headline))
         (ct (if (null contents) "" contents)))
    (format "<div><h%s id=\"%s\">%s</h%s>%s</div>" level id title level ct)))

(defun one-ox-section (_section contents _info)
  "Transcode a SECTION element from Org to HTML.

CONTENTS holds the contents of the section."
  (if (null contents) "" (format "<div>%s</div>" contents)))

(defun one-ox-paragraph (_paragraph contents _info)
  "Transcode a PARAGRAPH element from Org to HTML.

CONTENTS is the contents of the paragraph, as a string."
  (format "<p>%s</p>" contents))

(defun one-ox-plain-text (text _info)
  "Transcode a TEXT string from Org to HTML.

TEXT is the string to transcode."
  (one-escape text))

(defun one-ox-bold (_bold contents _info)
  "Transcode BOLD from Org to HTML.

CONTENTS is the text with bold markup."
  (format "<b>%s</b>" contents))

(defun one-ox-italic (_italic contents _info)
  "Transcode ITALIC from Org to HTML.

CONTENTS is the text with italic markup."
  (format "<i>%s</i>" contents))

(defun one-ox-strike-through (_strike-through contents _info)
  "Transcode STRIKE-THROUGH from Org to HTML.

CONTENTS is the text with strike-through markup."
  (format "<del>%s</del>" contents))

(defun one-ox-underline (_underline contents _info)
  "Transcode UNDERLINE from Org to HTML.

CONTENTS is the text with underline markup."
  (format "<u>%s</u>" contents))

(defun one-ox-code (code _contents _info)
  "Transcode CODE from Org to HTML."
  (format "<code class=\"one-hl one-hl-inline\">%s</code>"
          (one-escape (org-element-property :value code))))

(defun one-ox-verbatim (verbatim _contents _info)
  "Transcode VERBATIM from Org to HTML."
  (format "<code class=\"one-hl one-hl-inline\">%s</code>"
          (one-escape (org-element-property :value verbatim))))

(defun one-ox-plain-list (plain-list contents _info)
  "Transcode a PLAIN-LIST element from Org to HTML.

CONTENTS is the contents of the list."
  (let* ((type (pcase (org-element-property :type plain-list)
                 (`ordered "ol")
                 (`unordered "ul")
                 (other (error "`one-ox' doesn't support list type: %s" other)))))
    (format "<%s>%s</%s>" type contents type)))

(defun one-ox-item (_item contents _info)
  "Transcode an ITEM element from Org to HTML.

CONTENTS holds the contents of the item."
  (format "<li>%s</li>" contents))

(defun one-ox-table (_table contents _info)
  "Transcode a TABLE element from Org to HTML.

CONTENTS holds the contents of the table."
  (format "<table>%s</table>" contents))

(defun one-ox-table-row (table-row contents _info)
  "Transcode a TABLE-ROW element from Org to HTML.

CONTENTS holds the contents of the row."
  (if (eq 'rule (org-element-property :type table-row))
      ""
    (format "<tr>%s</tr>" contents)))

(defun one-ox-table-cell (table-cell contents _info)
  "Transcode a TABLE-CELL element from Org to HTML.

CONTENTS holds the contents of the cell."
  (let* ((row (org-export-get-parent table-cell))
         (table (org-export-get-parent-table table-cell))
         (has-header (org-export-table-has-header-p table _info))
         (row-number (org-export-table-row-number row _info))
         (tag (if (and has-header (= row-number 0)) "th" "td")))
    (format "<%s>%s</%s>" tag (or contents "") tag)))

(defun one-ox-no-subscript (_subscript contents _info)
  "Transcode a SUBSCRIPT object from Org to HTML.

CONTENTS is the contents of the object."
  (concat "_" contents))

(defun one-ox-no-superscript (_superscript contents _info)
  "Transcode a SUPERSCRIPT object from Org to HTML.

CONTENTS is the contents of the object."
  (concat "^" contents))

;;;; blocks

(defun one-ox-fontify-code (code lang)
  "Color CODE with htmlize library.

CODE is a string representing the source code to colorize.  LANG
is the language used for CODE, as a string, or nil."
  (when code
    (let* ((lang (or (assoc-default lang org-src-lang-modes) lang))
           (lang-mode (and lang (intern (format "%s-mode" lang)))))
      (if (functionp lang-mode)
          (let* ((code
                  (let ((inhibit-read-only t))
                    (with-temp-buffer
                      (funcall lang-mode)
                      (insert code)
                      (font-lock-ensure)
                      (org-src-mode)
                      (set-buffer-modified-p nil)
                      (let* ((htmlize-output-type 'css)
                             (htmlize-css-name-prefix "one-hl-")
                             (htmlbuf (htmlize-buffer)))
                        (unwind-protect
	                          (with-current-buffer htmlbuf
	                            (buffer-substring
                               (plist-get htmlize-buffer-places 'content-start)
			                         (plist-get htmlize-buffer-places 'content-end)))
                          (kill-buffer htmlbuf)))))))
            ;; Strip any enclosing <pre></pre> tags.
            (if-let ((beg (and (string-match "\\`<pre[^>]*>\n?" code) (match-end 0)))
                     (end (string-match "</pre>\\'" code)))
                (substring code beg end)
              code))
        (one-escape code)))))

(defun one-ox-htmlize (code lang &optional is-results-p)
  "Return CODE string htmlized using `htmlize.el' in language LANG.

If IS-RESULTS-P is non-nil, CSS class of tag <code> in the returned
string is \"one-hl one-hl-results\".
If nil, the CSS class is `one-hl one-hl-block'."
  (let* ((class (if is-results-p
                    "one-hl one-hl-results"
                  "one-hl one-hl-block")))
    (format "<pre><code class=\"%s\">%s</code></pre>"
            class
            (replace-regexp-in-string
             "<span class=\"one-hl-default\">\\([^<]*\\)</span>"
             "\\1"
             (if (null lang)
                 (one-escape (or code "")) ; example blocks
               (one-ox-fontify-code code lang))))))

(defun one-ox-src-block (src-block _contents _info)
  "Return SRC-BLOCK element htmlized using `htmlize.el'."
  (let* ((code (car (org-export-unravel-code src-block)))
         (lang (org-element-property :language src-block))
         (is-results-p (org-element-property :results src-block)))
    (one-ox-htmlize code lang is-results-p)))

(defun one-ox-example-block (example-block _contents _info)
  "Return EXAMPLE-BLOCK element htmlized using `htmlize.el'."
  (let* ((code (car (org-export-unravel-code example-block)))
         (lang "text")
         (is-results-p (org-element-property :results example-block)))
    (one-ox-htmlize code lang is-results-p)))

(defun one-ox-fixed-width (fixed-width _contents _info)
  "Return FIXED-WIDTH element htmlized using `htmlize.el'."
  (let* ((code (car (org-export-unravel-code fixed-width)))
         (lang "text")
         (is-results-p (org-element-property :results fixed-width)))
    (one-ox-htmlize code lang is-results-p)))

(defun one-ox-quote-block (_quote-block contents _info)
  "Transcode a QUOTE-BLOCK element from Org to HTML.

CONTENTS holds the contents of the block."
  (format "<blockquote class=\"one-blockquote\">%s</blockquote>" contents))

;;;; links

(define-error 'one-link-broken "Unable to resolve link")

(defvar one-ox-link-image-extensions
  (format "\\.%s\\'"
          (regexp-opt
           '("webp" "avif" "png" "jpeg" "jpg" "gif"
             "tiff" "tif" "xbm" "xpm" "pbm" "pgm" "ppm")
           t))
  "Regexp matching image extensions.

See `one-ox-link'.")

(defun one-ox-link (link desc info)
  "Transcode a LINK object from Org to HTML.
DESC is the description part of the link, or the empty string.
INFO is a plist holding contextual information."
  (let* ((type (org-element-property :type link))
         (path (org-element-property :path link))
         (raw-link (org-element-property :raw-link link))
         (custom-type-link
          (let ((export-func (org-link-get-parameter type :export)))
            (and (functionp export-func)
                 (funcall export-func path desc 'one-ox info))))
         (href (cond
                ((string= type "custom-id") path)
                ((string= type "fuzzy")
                 (let ((beg (org-element-property :begin link)))
                   (signal 'one-link-broken
                           `(,raw-link
                             "fuzzy links not supported"
                             ,(format "goto-char: %s" beg)))))
                ((string= type "file")
                 (or
                  ;; ./assets/images/image-1.png --> /images/image-1.png
                  ;; ./public/blog/page-1.md     --> /blog/page-1.md
                  (and (string-match "\\`\\./\\(assets\\|public\\)" path)
                       (replace-match "" nil nil path))
                  (let ((beg (org-element-property :begin link)))
                    (signal 'one-link-broken
                            `(,raw-link ,(format "goto-char: %s" beg))))))
                (t raw-link))))
    (or custom-type-link
        (and
         (string-match one-ox-link-image-extensions path)
         (format "<p><img src=\"%s\" alt=\"%s\" /></p>"
                 href (or (org-string-nw-p desc) href)) )
        (format "<a href=\"%s\">%s</a>"
                href (or (org-string-nw-p desc) href)))))

;;; Commands to build `one.el' web sites

(defvar one-add-to-global
  '((:one-global-property :one-tree
     :one-global-function (lambda (pages tree) tree)))
  "List used to set the `global' argument passed to render functions.

Elements in that list are plist with the following properties:

- `:one-global-property': a keyword that is used as proprety
  in the `global' argument passed to the render functions,
- `:one-global-function': a function that takes two arguments `pages'
  (list of pages, see `one-list-pages') and `tree'
  (see `one-parse-buffer').  That function is called once in
  `one-render-pages' and its result is used as the value of
  the property `:one-global-property' in the `global' argument
  passed to the render functions.

For instance, if `one-add-to-global' is set to

    ((:one-global-property :one-tree
      :one-global-function (lambda (pages tree) tree)))

then `global' local variable will be set to

    ((:one-tree tree))

where `tree' is the value returned by `one-parse-buffer' function.")

(defvar one-hook nil
  "List of functions called once in `one-render-pages'.

Those functions take three arguments:

- `pages': list of pages, see `one-list-pages',
- `tree': see `one-parse-buffer',
- `global': see `one-add-to-global'.

As those functions take `global' argument they are called after
that argument has been let binded using `one-add-to-global'.")

(defvar one-emacs-cmd-line-args-async nil
  "List of command line arguments to pass to `emacs' subprocess.

The function `one-render-pages-async' and `one-build-async' spawn an
`emacs' subprocess in order to build html pages asynchronously.  The
arguments passed to `emacs' depends on `one-emacs-cmd-line-args-async' value.

By default, when `one-emacs-cmd-line-args-async' is nil, we run `emacs'
in \"batch mode\", we load the user's initialization file and we evaluate
a specific sexp that builds html pages.  Specifically, we pass
the following `command' (`emacs' file name followed by command line
arguments) to `make-process' function like this:

    (let* ((emacs (file-truename
                   (expand-file-name invocation-name invocation-directory)))
           (command \\=`(,emacs \"--batch\"
                             \"-l\" ,user-init-file
                             \"--eval\" ,sexp))
           (sexp ...))
      (make-process
       :name ...
       :buffer ...
       :command command))

If `one-emacs-cmd-line-args-async' is non-nil, we no longer load the user's
initialization file and replace '\"-l\" ,user-init-file' in `command' above
by the elements of `one-emacs-cmd-line-args-async'.  For instance, if
`one-emacs-cmd-line-args-async' is equal to

    \\='(\"-l\" \"/path/to/some-elisp-file.el\")

then `command' becomes

    (let* (...
           (command \\=`(,emacs \"--batch\"
                             \"-l\" \"/path/to/some-elisp-file.el\"
                             \"--eval\" ,sexp))
           ...)
      ...)")

(define-error 'one-path "CUSTOM_ID not defined")

(defun one-internal-id (headline)
  "Return a string id for HEADLINE to be used as :one-internal-id property.

The id is built from CUSTOM_ID property of HEADLINE if set
or generated a randomly."
  (let* ((custom-id (org-element-property :CUSTOM_ID headline)))
    (or (and custom-id
             ;; we match "baz" in "/foo/bar/#baz"
             (string-match "\\`\\(?:[^#]+\\S-*\\)#\\(.+\\)" custom-id)
             (match-string-no-properties 1 custom-id))
        (format "one-%x" (random #x10000000000)))))

(defun one-parse-buffer ()
  "Parse current org buffer and return structure.

The only difference with `org-element-parse-buffer' is that
we add the property `:one-internal-id' to each headline."
  (let ((tree (org-element-parse-buffer)))
    ;; destructively add :one-internal-id property to headlines in `tree'
    (org-element-map tree 'headline
      (lambda (elt)
        (org-element-put-property
         elt :one-internal-id (one-internal-id elt))))
    tree))

(defun one-is-page (headline)
  "Return nil if HEADLINE element is not a `one.el' page.

If HEADLINE is a page, return a plist with the properties
`:one-title', `:one-path', `:one-render-page-function' and
`:one-page-tree' defined like this:

- `:one-title': the raw value of the first headline of HEADLINE,
- `:one-path': the path of the page as a string,
- `:one-render-page-function': the function to render the page as
  a symbol.  This function is declared in the org buffer for
  each page using the org property ONE.

  This function takes 3 arguments:

  - `page-tree' which correspond to the data in `:one-page-tree',
  - `pages' list of pages,
  - `global' a plist of global informations that are computed once
    when `one.el' website is built (before rendering the pages), see
    `one-render-pages' and `one-build'.  This argument can be
    modified by the user at build time.  That means that if your
    render function needs extra information you can tell `one.el' to
    compute those informations and to add them to `global'.

  You can see how to implement render functions looking at the
  default render functions:

  - `one-default-home',
  - `one-default-home-list-pages'
  - `one-default',
  - `one-default-with-toc',
  - `one-default-with-sidebar' and
  - `one-default-doc'.

- `:one-page-tree': the argument HEADLINE passed to `one-is-page'.

See `one-list-pages'."
  (when (= (org-element-property :level headline) 1)
    (when-let ((path (org-element-property :CUSTOM_ID headline))
               (render-page-function (org-element-property :ONE headline)))
      `(:one-title ,(org-element-property :raw-value headline)
        :one-path ,path
        :one-render-page-function ,(intern render-page-function)
        :one-page-tree ,headline))))

(defun one-list-pages (tree)
  "Return the list of the pages in TREE.

TREE is a parsed org buffer as returned by `one-parse-buffer'.
The function `one-is-page' determines which headlines in TREE
are pages."
  (org-element-map tree 'headline
    (lambda (headline) (one-is-page headline))))

(defun one-render-page (page pages global)
  "Render the webpage PAGE.

See `one-is-page' for the meaning of PAGES and GLOBAL argument."
  (let* ((path (concat "./public" (plist-get page :one-path)))
         (file (concat path "index.html"))
         (render-page-function (plist-get page :one-render-page-function))
         (page-tree (plist-get page :one-page-tree)))
    (make-directory path t)
    (with-temp-file file
      (insert (funcall render-page-function page-tree pages global)))))

;;;###autoload
(defun one-render-pages (&optional one-path)
  "Render webpages of the current buffer under `./public/' dir.

If ONE-PATH is non-nil, it must be the path of page in the current
buffer.  That means it must match the `CUSTOM_ID' org property value
of a level 1 headline.

If a file `onerc.el' exist in the current directory, it will be loaded
first.  This way we can customize how the website is built by adding
some Elisp code in that file.

Information in `one-add-to-global' are used to set the ‘global’ argument
passed to render functions.

Once `onerc.el' file has been loaded and `global' argument passed to
render functions set the hook `one-hook' is run.  Then the webpages
are rendered.

The current buffer should look like this.

    ---------- Buffer ----------
    * Home
    :PROPERTIES:
    :ONE: render-function-0
    :CUSTOM_ID: /
    :END:

    Content of the home page

    * Page 1
    :PROPERTIES:
    :ONE: render-function-1
    :CUSTOM_ID: /blog/page-1/
    :END:

    Content of page 1

    * Page 2
    :PROPERTIES:
    :ONE: render-function-2
    :CUSTOM_ID: /blog/page-2/
    :END:

    Content of page 2

    * I'm not a Page and I won't be rendered
    ---------- Buffer ----------

Each level 1 headline with the org properties `ONE' and
`CUSTOM_ID' set is a page.  See `one-is-page'.

How those pages are rendered and where they are rendered
depends on the render functions specified in `ONE' org
property and the path specified in `CUSTOM_ID' org property.

For instance, with the above buffer, assuming the render
functions `render-function-0' and `render-function-2'
are well defined and assuming the render function
`render-function-1' is defined like this

    (defun render-function-1 (page-tree pages global)
      \"<h1>Hello world!</h1>\")

calling the command `one-render-pages' produces
the following files

    .
    └── public
        ├── blog
        │   ├── page-1
        │   │   └── index.html
        │   └── page-2
        │       └── index.html
        └── index.html

and the content of the file `./public/blog/page-1/index.html' is

    ---------- File: ./public/blog/page-1/index.html ----------
    <h1>Hello world!</h1>
    ---------- File: ./public/blog/page-1/index.html ----------

Therefore if you serve the website in `./public/' directory at
`http://localhost:3000' you can access the \"Hello word!\" page
at `http://localhost:3000/blog/page-1/'.

You can see how to implement render functions looking at the
implementation of the default render functions `one-default-home',
`one-default-home-list-pages',`one-default', `one-default-with-toc',
`one-default-with-sidebar' and `one-default-doc'.

Note that `one-render-pages' doesn't copy files from
`./assets/' directory to `./public/' directory.

See `one-build'.

If you want to start a new `one.el' project with the default style
see `one-default-new-project' command.

Note: I use https://browsersync.io to serve the website in `./public/'
directory.  Once you have it installed, to start a web server with
live reloading, you can run the following commands (in a terminal):

    $ cd public
    $ browser-sync start -s -w --files \"*\""
  (interactive)
  (let ((onerc (concat default-directory "onerc.el"))
        (inhibit-message t))
    (when (file-exists-p onerc) (load onerc)))
  (let* ((tree (org-with-wide-buffer (one-parse-buffer)))
         (pages (one-list-pages tree))
         (global
          (let (global)
            (dolist (glob one-add-to-global)
              (push (funcall (plist-get glob :one-global-function) pages tree)
                    global)
              (push (plist-get glob :one-global-property) global))
            global)))
    (dolist (hook one-hook) (funcall hook pages tree global))
    (if one-path
        (if-let ((page (seq-some
                        (lambda (page)
                          (when (string= (plist-get page :one-path) one-path)
                            page))
                        pages)))
            (progn
              (message "Build page `%s'..." one-path)
              (one-render-page page pages global)
              (message "Build page `%s'...done" one-path))
          (error "Page `%s' doesn't exist" one-path))
      (message "Build pages...")
      (dolist (page pages)
        (progn
          (message "Build page `%s'" (plist-get page :one-path))
          (one-render-page page pages global)))
      (message "Build pages...done"))))

;;;###autoload
(defun one-render-pages-async (&optional one-path)
  "Render webpages of the current buffer under `./public/' dir asynchronously.

The function `one-render-pages-async' spawns an `emacs' subprocess
in order to build html pages asynchronously.  The arguments passed to
`emacs' depends on `one-emacs-cmd-line-args-async' value.

If ONE-PATH is non-nil, it must be the path of page in the current
buffer.  That means it must match the `CUSTOM_ID'org property value
of a level 1 headline.

See `one-render-pages'."
  (interactive)
  (let* ((org-content (org-with-wide-buffer
                       (buffer-substring (point-min) (point-max))))
         (org-content-file (make-temp-file "one-content-" nil ".org"))
         (current-dir default-directory)
         (sexp (with-output-to-string
                 (prin1 `(progn
                           (require 'one)
                           (find-file ,org-content-file)
                           (setq default-directory ,current-dir)
                           (one-render-pages ,one-path)))))
         (emacs (file-truename
                 (expand-file-name invocation-name invocation-directory)))
         (command (if one-emacs-cmd-line-args-async
                      `(,emacs "--batch" ,@one-emacs-cmd-line-args-async "--eval" ,sexp)
                    `(,emacs "--batch" "-l" ,user-init-file "--eval" ,sexp)))
         (sentinel (lambda (process msg)
                     (internal-default-process-sentinel process msg)
                     (if (string-match-p "finished" msg)
                         (if one-path
                             (message "Build page `%s'...done" one-path)
                           (message "Build pages...done"))
                       (message "%s, check buffer `%s'"
                                (string-trim-right msg)
                                (buffer-name (process-buffer process)))))))
    (with-temp-file org-content-file (insert org-content))
    (if one-path
        (message "Build page `%s'..." one-path)
      (message "Build pages..."))
    (let ((process-connection-type nil)
          (inhibit-message t))
      (make-process
       :name "one"
       :buffer (get-buffer-create "*one*")
       :command command
       :connection-type nil
       :sentinel sentinel))))

(defun one-page-at-point ()
  "Return `one-path' of the page at point.

Return nil if no page found.
Doesn't move point nor change the match data."
  (save-match-data
    (save-excursion
      (org-with-wide-buffer
       (goto-char (line-beginning-position))
       (when (not (looking-at "^\\* "))
         (search-backward-regexp "^\\* " nil t))
       (org-element-property :CUSTOM_ID (org-element-at-point))))))

;;;###autoload
(defun one-render-page-at-point ()
  "Build page at point.

See `one-render-pages'."
  (interactive)
  (if-let ((one-path (one-page-at-point)))
      (one-render-pages one-path)
    (message "No page found at point")))

;;;###autoload
(defun one-render-page-at-point-async ()
  "Build page at point asynchronously.

See `one-render-pages-async'."
  (interactive)
  (if-let ((one-path (one-page-at-point)))
      (one-render-pages-async one-path)
    (message "No page found at point")))

(defun one-copy-assets-to-public ()
  "Copy `./assets/' files into `./public/' subdirectory."
  (interactive)
  (when (file-exists-p "./assets/")
    (copy-directory "./assets/" "./public/" nil t 'copy-contents)))

;;;###autoload
(defun one-build-async ()
  "Build website of the current buffer under `./public/' dir asynchronously.

The function `one-build-async' spawns an `emacs' subprocess in order to
render html pages, clean `./public/' directory and copy `./assets/'
directory asynchronously.  The arguments passed to `emacs' depends
on `one-emacs-cmd-line-args-async' value.

See `one-build'."
  (interactive)
  (let* ((org-content (org-with-wide-buffer
                       (buffer-substring (point-min) (point-max))))
         (org-content-file (make-temp-file "one-content-" nil ".org"))
         (current-dir default-directory)
         (sexp (with-output-to-string
                 (prin1 `(progn
                           (find-file ,org-content-file)
                           (setq default-directory ,current-dir)
                           (when (file-exists-p "./public/")
                             (dolist (file (cddr (directory-files "./public/" 'full)))
                               (if (file-directory-p file)
                                   (delete-directory file t)
                                 (delete-file file))))
                           (require 'one)
                           (one-copy-assets-to-public)
                           (one-render-pages)))))
         (emacs (file-truename
                 (expand-file-name invocation-name invocation-directory)))
         (command (if one-emacs-cmd-line-args-async
                      `(,emacs "--batch" ,@one-emacs-cmd-line-args-async "--eval" ,sexp)
                    `(,emacs "--batch" "-l" ,user-init-file "--eval" ,sexp)))
         (sentinel (lambda (process msg)
                     (internal-default-process-sentinel process msg)
                     (if (string-match-p "finished" msg)
                         (message "Build pages...done")
                       (message "%s, check buffer `%s'"
                                (string-trim-right msg)
                                (buffer-name (process-buffer process)))))))
    (with-temp-file org-content-file (insert org-content))
    (message "Build pages...")
    (let ((process-connection-type nil)
          (inhibit-message t))
      (make-process
       :name "one"
       :buffer (get-buffer-create "*one*")
       :command command
       :connection-type nil
       :sentinel sentinel))))

;;;###autoload
(defun one-build ()
  "Build website of the current buffer under `./public/' subdirectory.

Specifically:

1) clean `./public/' subdirectory (if it exists),
2) copy `./assets/' files into `./public/' subdirectory and
3) call `one-render-pages' once.

See `one-render-pages'."
  (interactive)
  (when (file-exists-p "./public/")
    (dolist (file (cddr (directory-files "./public/" 'full)))
      (if (file-directory-p file)
          (delete-directory file t)
        (delete-file file))))
  (one-copy-assets-to-public)
  (one-render-pages))

;;; A default web site

(defvar one-default-css
  "@import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400&family=Noto+Sans:wght@400;700&display=swap');

html, body, p, ol, ul, li, dl, dt, dd,
blockquote, figure, fieldset, legend, textarea,
pre, iframe, hr, h1, h2, h3, h4, h5, h6 {
  margin: 0;
  padding: 0;
}

*, *::before, *::after {
  box-sizing: border-box;
}

p, blockquote, ul, ol, code,
dl, table, pre, details {
  margin-bottom: 16px;
  margin-top: 0;
}

ul {
  padding-left: 2em;
  list-style: disc;
}

ul ul {
  margin-top: 0;
  margin-bottom: 0;
}

ol {
  padding-left: 2em;
  list-style: decimal;
}

li p:first-of-type {
  margin: 0;
}

li p {
  margin: 16px 0;
}

li code {
  margin: 16px 0;
}

html {
  scroll-padding-top: 4rem; /* because we use a sticky header */
}

body {
  background: #151515;
  color: #dedede;
  font-family: \"Noto Sans\",sans-serif;
	font-size: 106%;
  line-height: 1.5;
  word-wrap: break-word;
}

h1 {
  font-size: 2em;
}

h2, h3, h4, h5, h6 {
  padding-bottom: 0.3em;
  margin-top: 24px;
  margin-bottom: 16px;
  font-weight: bold;
  line-height: 1.25;
}

h2, h3 {
  border-bottom: 1px solid #1d272b;
}

h2 {font-size: 2em;}
h3 {font-size: 1.5em;}
h4 {font-size: 1.25em;}
h5 {font-size: 1em;}
h6 {font-size: .875em;}

a {
  color: #ffd787;
  cursor: pointer;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

a:visited {
  color: #ffd787;
}

img {
  width: 100%;
  height: auto;
  border-radius: 6px;
}

/* ------- '.one' classes used by 'one-ox' org backend ------- */

.one-hl {
	font-family: 'Fira Mono', monospace;
  font-size: 80%;
  border-radius: 6px;
}

.one-hl-inline {
	background: #31424a;
  padding: 0.2em 0.4em;
  margin: 0;
  white-space: break-spaces;
}

.one-hl-block {
  background: #161f22;
  color: #c5c5c5;
  display: block;
  overflow: auto;
  padding: 16px;
  line-height: 1.45;
}

.one-blockquote {
  background: #202d31;
  border-left: 0.3em solid #31424a;
  margin: 0px auto 16px;
  padding: 1em 1em;
  width: 90%;
}

.one-blockquote > p:last-child {
  margin-bottom: 0;
}

.one-hl-results {
  background: #202d31 ;
  border-left: 2px solid #c5c5c5;
  display: block;
  margin: auto;
  padding: 0.5em 1em;
  overflow: auto;
  width: 98%;
}

.one-hl-negation-char { color: #ff6c60}              /* font-lock-negation-char-face */
.one-hl-warning { color: #fd971f}                    /* font-lock-warning-face */
.one-hl-variable-name { color: #fd971f}              /* font-lock-variable-name-face */
.one-hl-doc { color: #d3b2a1}                        /* font-lock-doc-face */
.one-hl-doc-string { color: #d3b2a1}                 /* font-lock-doc-string-face */
.one-hl-string { color: #d3b2a1}                     /* font-lock-string-face */
.one-hl-function-name { color: #02d2da}              /* font-lock-function-name-face */
.one-hl-builtin { color: #b2a1d3}                    /* font-lock-builtin-face */
.one-hl-type { color: #457f8b}                       /* font-lock-type-face */
.one-hl-keyword { color: #f92672}                    /* font-lock-keyword-face */
.one-hl-preprocessor { color: #f92672}               /* font-lock-preprocessor-face */
.one-hl-comment-delimiter { color: #8c8c8c}          /* font-lock-comment-delimiter-face */
.one-hl-comment { color: #8c8c8c}                    /* font-lock-comment-face */
.one-hl-constant { color: #f5ebb6}                   /* font-lock-constant-face */
.one-hl-reference { color: #f5ebb6}                  /* font-lock-reference-face */
.one-hl-regexp-grouping-backslash { color: #966046}  /* font-lock-regexp-grouping-backslash */
.one-hl-regexp-grouping-construct { color: #aa86ee}  /* font-lock-regexp-grouping-construct */
.one-hl-number { color: #eedc82}                     /* font-lock-number-face */

.one-hl-sh-quoted-exec { color: #62bd9c}             /* sh-quoted-exec */

/* -------- scrollbar -------- */

::-webkit-scrollbar {
  width: 1em;
  height: 1em;
}

::-webkit-scrollbar-track {
  background: #202d31;
}

::-webkit-scrollbar-thumb {
  background: #31424a;
  border-radius: 0.5em;
}

::-webkit-scrollbar-thumb:hover {
  background: #31424a;
}

/* -------- specific to the default render functions -------- */

.header {
  color: #ffffff;
  font-size: 2em;
  font-weight: bold;
  padding: 0 16px 0 16px;
  background: #151515;
  width: 100%;
  height: 3.5rem;
  position: fixed;
  top: 0;
  left: 0;
  border-bottom: 1px solid #1d272b;
  display: flex;
  justify-content: center;
  align-items: center;
}

.header > a {
  color: inherit;
  cursor: pointer;
  text-decoration: none;
}

.header > a:visited {
  color: inherit;
}

.content {
  margin: 3.5rem auto;
  padding-top: 1.8rem;
  max-width: 740px;
  padding: 0 16px;
}

.title {
  text-align: center;
  padding: 1.8rem 0;
}

.title-empty {
  padding-top: 1rem;
}

/* -------- one-default-home -------- */

#home {
  margin: 5rem 0 1.5rem 0;
}

/* -------- one-default-home-list-pages -------- */

#home-list-pages {
  margin: 5rem 0 1.5rem 0;
}

#pages ul {
  padding: 0;
  list-style: none;
}

#pages a {
  display: block;
  line-height: 1.2em;
  font-size: 1.2em;
  color: #dedede;
  border-bottom: 1px solid #1d272b;
  padding: 1em 0.3em;
}

#pages a:hover {
  text-decoration: none;
  background: #31424a;
  color: #ffffff;
}

/* -------- one-default, one-default-with-toc, one-default-with-sidebar, one-default-doc -------- */

.nav {
  border-top: 1px solid #c5c5c5;
  margin-top: 3em;
  padding: 2em 0;
  display: flex;
  justify-content: center;
  gap: 0.5em;
  font-weight: bold;
}

.nav a {
  display: block;
  background: #dedede;
  border-radius: 6px;
  padding: 0.2em 0.8em;
  color: #151515;
  width: 20%;
  text-align: center;
}

@media (max-width:600px) {
  .nav a {
    width: auto;
  }
}

/* -------- one-default-with-toc, one-default-doc -------- */

.toc {
  display: flex;
  justify-content: center;
  margin-bottom: 1.8rem;
  color: #d1d1d1;
}

.toc > div {
  padding: 0 1em;
}

.toc a {
  color: #d1d1d1;
}

.toc > div > div:first-child {
  text-decoration: underline 1px;
  text-align: center;
  font-size: 1.2em;
  margin-bottom: 16px;
}

/* --------- one-default-with-sidebar, one-default-doc --------- */

#sidebar-header {
  color: #ffffff;
  font-size: 2em;
  font-weight: bold;
  padding: 0 16px 0 16px;
  background: #151515;
  width: 100%;
  height: 3.5rem;
  position: fixed;
  top: 0;
  left: 0;
  border-bottom: 1px solid #1d272b;
  display: flex;
  justify-content: center;
  align-items: center;
}

#sidebar-header > a {
  color: inherit;
  cursor: pointer;
  text-decoration: none;
}

#sidebar-header > a:visited {
  color: inherit;
}

#hamburger {
  cursor: pointer;
  height: 1em;
  fill: #dedede;
  display: none;
  font-weight: normal;
  margin-right: 0.3em;
}

#sidebar-content {
  margin: 3.5rem auto;
  display: flex;
  margin-left: auto;
  margin-right: auto;
  max-width: 1140px;
  width: 100%;
  padding: 1em 16px;
}

#sidebar {
  border-right: 2px solid #31424a;
  top: 4.5rem;
  position: sticky;
  padding-top: 2.2em;
  padding-bottom: 6em;
  width: 250px;
  max-height: 100vh;
  overflow-y: auto;
}

#sidebar a {
  display: block;
  color: #dedede;
}

#sidebar a:hover {
  text-decoration: none;
}

#sidebar ul {
  list-style: none;
  padding:0;
}

#sidebar li {
  padding: 0.5em 0.6em;
}

#sidebar li:hover {
  background: #31424a;
}

article {
  padding: 0 1.5em;
  max-width: 640px;
  width: 100%;
}

#sidebar-left {
  width: 0;
  height: 100%;
  position: fixed;
  z-index: 3;
  top: 0;
  left: 0;
  transition: 0.25s;
	background: #2c444f;
  overflow: hidden; /* to make the children disappear when width is 0 */
  overflow-y: auto;
}

#sidebar-left > div:first-child {
  height: 3.5rem;
  font-size: 2em;
  font-weight: bold;
  border-bottom: 1px solid #b8b8b8;
  padding-left: 16px;
  margin-bottom: 16px;
  display: flex;
  align-items: center;
}

#sidebar-left > ul {
  padding: 0 16px 0 16px;
}

#sidebar-left > ul ul {
  padding-left: 0.8em;
  margin-left: 3px;
  border-left: 1px solid #b8b8b8;
}

#sidebar-left a {
  color: #dedede;
  text-decoration: none;
}

#sidebar-left li {
  padding: 0.5em 0;
  list-style-type: none;
}

#sidebar-main {
  display: none;
  top: 0;
  right: 0;
  width: 100%;
  height: 100%;
  position: fixed;
  background: #080808;
  opacity: 0.80;
  z-index: 2;
}

@media (max-width: 840px) {
  #hamburger {
    display: block;
  }
  #sidebar {
    display: none;
  }
  #sidebar-content {
    justify-content: center;
  }
  #sidebar-header {
    justify-content: left;
  }
  article {
    padding: 0;
  }
}
"
  "Default CSS style sheet.

This style sheet is meant to be used with the default render functions
`one-default-home', `one-default-home-list-pages', `one-default',
`one-default-with-toc', `one-default-with-sidebar' and `one-default-doc'.

See `one-default-new-project' and `one-default-add-css-file'.")

(defvar one-default-org-content
  "* one.el
:PROPERTIES:
:ONE: one-default-home
:CUSTOM_ID: /
:END:

This is a new ~one.el~ project.

If you don't know how ~one.el~ works, you can check the documentation at
https://one.tonyaldon.com.

If you want to list all the pages on your website on the home page,
check [[#/blog/default-home-list-pages/][List all website's pages on the home page]].

* List all website's pages on the home page
:PROPERTIES:
:ONE: one-default-home-list-pages
:CUSTOM_ID: /blog/default-home-list-pages/
:END:

This page is rendered with the default render function
~one-default-home-list-pages~ specified in ~ONE~ org property which lists
below all the pages on the website.  You can use it instead of
~one-default-home~ for your home page.

* The default page
:PROPERTIES:
:ONE: one-default
:CUSTOM_ID: /blog/default/
:END:

This page is rendered with the default render function ~one-default~
specified in ~ONE~ org property.

** Do you want a table of content?

As you can see, ~one-default~ doesn't add a table of content (TOC).  If
you want a default render function that adds the TOC to the page you can
use the render function ~one-default-with-toc~ presented in [[#/blog/one-default-with-toc/][The default
page with a TOC]].

** Headline foo
*** Headline bar

Some content.

*** Headline baz

#+BEGIN_SRC bash :results verbatim
tree
#+END_SRC

#+RESULTS:
#+begin_example
.
├── assets
│   └── one.css
├── one.org
└── public
    ├── blog
    │   ├── default
    │   │   └── index.html
    │   ├── default-home-list-pages
    │   │   └── index.html
    │   ├── one-default-doc
    │   │   └── index.html
    │   ├── one-default-with-sidebar
    │   │   └── index.html
    │   └── one-default-with-toc
    │       └── index.html
    ├── index.html
    └── one.css

8 directories, 9 files
#+end_example

* The default page with a TOC
:PROPERTIES:
:ONE: one-default-with-toc
:CUSTOM_ID: /blog/one-default-with-toc/
:END:

This page is rendered with the render function ~one-default-with-toc~
specified in the org property ~ONE~.

** Do you want a sidebar?

Perhaps you want a sidebar listing all the pages on your website, as
many modern documentation sites do.  If so, you can use the default
render function ~one-default-with-sidebar~ presented in [[#/blog/one-default-with-sidebar/][The default page
with a sidebar]].

** Headline foo
*** Headline bar

Some content.

*** Headline baz

#+BEGIN_SRC bash :results verbatim
tree
#+END_SRC

#+RESULTS:
#+begin_example
.
├── assets
│   └── one.css
├── one.org
└── public
    ├── blog
    │   ├── default
    │   │   └── index.html
    │   ├── default-home-list-pages
    │   │   └── index.html
    │   ├── one-default-doc
    │   │   └── index.html
    │   ├── one-default-with-sidebar
    │   │   └── index.html
    │   └── one-default-with-toc
    │       └── index.html
    ├── index.html
    └── one.css

8 directories, 9 files
#+end_example

* The default page with a sidebar
:PROPERTIES:
:ONE: one-default-with-sidebar
:CUSTOM_ID: /blog/one-default-with-sidebar/
:END:

This page is rendered with the render function ~one-default-with-sidebar~
specified in the org property ~ONE~.

** Do you want a sidebar and a TOC?

Perhaps you want a sidebar listing all the pages on your website and a
table of content, as many modern documentation sites do.  If so, you
can use the default render function ~one-default-doc~ presented in [[#/blog/one-default-doc/][The
default page with TOC and sidebar]].

** Headline foo
*** Headline bar

Some content.

*** Headline baz

#+BEGIN_SRC bash :results verbatim
tree
#+END_SRC

#+RESULTS:
#+begin_example
.
├── assets
│   └── one.css
├── one.org
└── public
    ├── blog
    │   ├── default
    │   │   └── index.html
    │   ├── default-home-list-pages
    │   │   └── index.html
    │   ├── one-default-doc
    │   │   └── index.html
    │   ├── one-default-with-sidebar
    │   │   └── index.html
    │   └── one-default-with-toc
    │       └── index.html
    ├── index.html
    └── one.css

8 directories, 9 files
#+end_example

* The default page with TOC and sidebar
:PROPERTIES:
:ONE: one-default-doc
:CUSTOM_ID: /blog/one-default-doc/
:END:

This page is rendered with the function ~one-default-doc~ specified
in the org property ~ONE~.

** Do you want to know more about one.el?

Check the documentation at https://one.tonyaldon.com.

** Headline foo
*** Headline bar

Some content.

*** Headline baz

#+BEGIN_SRC bash :results verbatim
tree
#+END_SRC

#+RESULTS:
#+begin_example
.
├── assets
│   └── one.css
├── one.org
└── public
    ├── blog
    │   ├── default
    │   │   └── index.html
    │   ├── default-home-list-pages
    │   │   └── index.html
    │   ├── one-default-doc
    │   │   └── index.html
    │   ├── one-default-with-sidebar
    │   │   └── index.html
    │   └── one-default-with-toc
    │       └── index.html
    ├── index.html
    └── one.css

8 directories, 9 files
#+end_example
"
  "Default org file to start a new `one.el' project.

See `one-default-new-project'.")

(defun one-default-add-css-file ()
  "Add default css file `./assets/one.css' with the content `one-default-css'.

See `one-default-new-project'.

See `one-default-home', `one-default-home-list-pages',`one-default',
`one-default-with-toc', `one-default-with-sidebar' and `one-default-doc'."
  (interactive)
  (make-directory "assets" t)
  (with-temp-file "./assets/one.css" (insert one-default-css)))

;;;###autoload
(defun one-default-new-project ()
  "Initialize a new project in the current directory with the default style.

It is structured like this:

    .
    ├── assets
    │   └── one.css
    └── one.org

The content of the file `./assets/one.css' is `one-default-css'.
The content of the file `./one.org' is `one-default-org-content'.

Once you've initialized this new `one.el' project, you can build it
calling `one-build' command while visiting the file `./one.org'.
This results in producing the website under the subdirectory `./public/'.

See `one-render-pages'."
  (interactive)
  (one-default-add-css-file)
  (with-temp-file "one.org" (insert one-default-org-content))
  (find-file "one.org"))

(defun one-default-home (page-tree pages _global)
  "Default render function to use in the home page.

See `one-is-page' for the meaning of PAGE-TREE and PAGES.

Also see `one-render-pages' and `one-default-css'."
  (let* ((title (org-element-property :raw-value page-tree))
         (content (org-export-data-with-backend
                   (org-element-contents page-tree)
                   'one-ox nil))
         (website-name (one-default-website-name pages)))
    (jack-html
     "<!DOCTYPE html>"
     `(:html
       (:head
        (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
        (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css"))
        (:title ,title))
       (:body
        (:div.header ,website-name)
        (:div.content
         (:div/home ,content)))))))

(defun one-default-home-list-pages (page-tree pages _global)
  "Default render function to use in the home page that lists pages.

See `one-is-page' for the meaning of PAGE-TREE and PAGES.

Also see `one-render-pages' and `one-default-css'."
  (let* ((title (org-element-property :raw-value page-tree))
         (content (org-export-data-with-backend
                   (org-element-contents page-tree)
                   'one-ox nil))
         (website-name (one-default-website-name pages))
         ;; All pages but the home pages
         (pages-list (one-default-pages pages "/.+")))
    (jack-html
     "<!DOCTYPE html>"
     `(:html
       (:head
        (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
        (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css"))
        (:title ,title))
       (:body
        (:div.header (:a (@ :href "/") ,website-name))
        (:div.content
         (:div/home-list-pages ,content)
         (:div/pages (:ul ,(reverse pages-list)))))))))

(defun one-default (page-tree pages _global)
  "Default render function.

See `one-is-page' for the meaning of PAGE-TREE and PAGES.

Also see `one-render-pages' and `one-default-css'."
  (let* ((title (org-element-property :raw-value page-tree))
         (path (org-element-property :CUSTOM_ID page-tree))
         (content (org-export-data-with-backend
                   (org-element-contents page-tree)
                   'one-ox nil))
         (website-name (one-default-website-name pages))
         (nav (one-default-nav path pages)))
    (jack-html
     "<!DOCTYPE html>"
     `(:html
       (:head
        (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
        (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css"))
        (:title ,title))
       (:body
        (:div.header (:a (@ :href "/") ,website-name))
        (:div.content
         (:div.title
          ,(if (not (string= path "/"))
               `(:div.title (:h1 ,title))
             '(:div.title-empty)))
         ,content
         ,nav))))))

(defun one-default-toc-component (headlines)
  "Return the table of content from HEADLINES as a `jack-html' component.

Return nil if HEADLINES is nil.

See `one-default-list-headlines' and `one-default-toc'."
  (when headlines
    `(:div.toc
      (:div
       (:div "Table of content")
       (:div ,(one-default-toc headlines))))))

(defun one-default-with-toc (page-tree pages _global)
  "Default render function with a table of content.

See `one-is-page' for the meaning of PAGE-TREE and PAGES.

Also see `one-render-pages' and `one-default-css'."
  (let* ((title (org-element-property :raw-value page-tree))
         (path (org-element-property :CUSTOM_ID page-tree))
         (content (org-export-data-with-backend
                   (org-element-contents page-tree)
                   'one-ox nil))
         (website-name (one-default-website-name pages))
         (headlines (cdr (one-default-list-headlines page-tree)))
         (toc (one-default-toc-component headlines))
         (nav (one-default-nav path pages)))
    (jack-html
     "<!DOCTYPE html>"
     `(:html
       (:head
        (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
        (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css"))
        (:title ,title))
       (:body
        (:div.header (:a (@ :href "/") ,website-name))
        (:div.content
         (:div.title
          ,(if (not (string= path "/"))
               `(:div.title (:h1 ,title))
             '(:div.title-empty)))
         ,toc
         ,content
         ,nav))))))

(defun one-default-sidebar (page-tree pages _global &optional with-toc)
  "Return a HTML string with PAGES listed in a sidebar.

The arguments PAGE-TREE, PAGES and _GLOBAL are the same as
render functions take (See `one-is-page').

When WITH-TOC is non-nil, add the table of content of PAGE-TREE
in the HTML string.

This function is meant to be used by `one-default-with-sidebar'
and `one-default-doc' render functions.

See `one-render-pages', `one-default-css' and `one-default-pages'."
  (let* ((title (org-element-property :raw-value page-tree))
         (path (org-element-property :CUSTOM_ID page-tree))
         (content (org-export-data-with-backend
                   (org-element-contents page-tree)
                   'one-ox nil))
         (website-name (one-default-website-name pages))
         (pages-list (one-default-pages pages))
         (headlines (cdr (one-default-list-headlines page-tree)))
         (toc (one-default-toc-component headlines))
         (nav (one-default-nav path pages)))
    (jack-html
     "<!DOCTYPE html>"
     `(:html
       (:head
        (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1"))
        (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css"))
        (:title ,title))
       (:body
        ;; sidebar-left and sidebar-main are for small devices
        (:div/sidebar-left (@ :onclick "followSidebarLink()")
         (:div (:div "Pages"))
         ,pages-list)
        (:div/sidebar-main)
        (:div/sidebar-header
         (:svg/hamburger (@ :viewBox "0 0 24 24" :onclick "sidebarShow()")
          (:path (@ :d "M21,6H3V5h18V6z M21,11H3v1h18V11z M21,17H3v1h18V17z")))
         (:a (@ :href "/") ,website-name))
        (:div/sidebar-content
         (:div/sidebar ,pages-list)
         (:article
          ,(if (not (string= path "/"))
               `(:div.title (:h1 ,title))
             '(:div.title-empty))
          ,(when with-toc toc)
          ,content
          ,nav)))
       (:script "
function sidebarShow() {
  if (window.innerWidth < 481)
    document.getElementById('sidebar-left').style.width = '75vw';
  else {
    document.getElementById('sidebar-left').style.width = 'min(300px, 34vw)';
  }
  document.getElementById('sidebar-main').setAttribute('onclick', 'sidebarHide()');
  document.getElementById('sidebar-main').style.display = 'block';
}
function sidebarHide() {
  document.getElementById('sidebar-left').style.width = '0';
  document.getElementById('sidebar-main').style.display = 'none';
}
")))))

(defun one-default-with-sidebar (page-tree pages global)
  "Default render function with a sidebar listing PAGES.

See `one-is-page' for the meaning of PAGE-TREE and GLOBAL.

Also see `one-default-sidebar', `one-render-pages' and `one-default-css'."
  (one-default-sidebar page-tree pages global))

(defun one-default-doc (page-tree pages global)
  "Default render function with a sidebar listing PAGES and the table of content.

See `one-is-page' for the meaning of PAGE-TREE and GLOBAL.

Also see `one-default-sidebar', `one-render-pages' and `one-default-css'."
  (one-default-sidebar page-tree pages global 'with-toc))

(defun one-default-pages (pages &optional filter)
  "Return `jack-html' list of PAGES component.

If FILTER is non-nil, a page is listed only when its path (value
of `:one-path' property) matches FILTER regexp.

Evaluating the following form

    (one-default-pages
     \\='((:one-title \"HOME\" :one-path \"/\")
       (:one-title \"FOO-1\" :one-path \"/foo-1/\")
       (:one-title \"FOO-2\" :one-path \"/foo-2/\")))

returns:

    (:ul
     (:li (:a (@ :href \"/\") \"HOME\"))
     (:li (:a (@ :href \"/foo-1/\") \"FOO-1\"))
     (:li (:a (@ :href \"/foo-2/\") \"FOO-2\")))

And evaluating the following form with the filter \"/.+\"

    (one-default-pages
     \\='((:one-title \"HOME\" :one-path \"/\")
       (:one-title \"FOO-1\" :one-path \"/foo-1/\")
       (:one-title \"FOO-2\" :one-path \"/foo-2/\"))
       \"/.+\")

returns a list which doesn't include the home page:

    (:ul
     (:li (:a (@ :href \"/foo-1/\") \"FOO-1\"))
     (:li (:a (@ :href \"/foo-2/\") \"FOO-2\")))"
  (when-let ((li-items
              (delq nil
                    (mapcar
                     (lambda (page)
                       (let ((href (plist-get page :one-path))
                             (title (plist-get page :one-title)))
                         (when (string-match-p (or filter ".*") href)
                           `(:li (:a (@ :href ,href) ,title)))))
                     pages))))
    `(:ul ,@li-items)))

(defun one-default-website-name (pages)
  "Return the website's name.

This corresponds to the title (value of the property `:one-title')
of the page in PAGES whom path is \"/\" (the home page).
Return nil if the home page is not part of PAGES.

See `one-default-home', `one-default-home-list-pages', `one-default',
`one-default-with-toc', `one-default-with-sidebar' and `one-default-doc'."
  (seq-some
   (lambda (page)
     (when (string= (plist-get page :one-path) "/")
       (plist-get page :one-title)))
   pages))

(defun one-default-nav (path pages)
  "Return `jack-html' navigation component.

The component is composed of 3 links:

- \"PREV\": link to the page before the page whose `:one-path' is
  equal to PATH in PAGES,
- \"RANDOM\": link to a random page picked in PAGES whose `:one-path'
  is not equal to PATH,
- \"NEXT\": link to the page after the page whose `:one-path' is
  equal to PATH in PAGES.

For instance, evaluating the following form

    (one-default-nav \"/foo-2/\"
                     \\='((:one-path \"/\")
                       (:one-path \"/foo-1/\")
                       (:one-path \"/foo-2/\")
                       (:one-path \"/foo-3/\")
                       (:one-path \"/foo-4/\")))

returns (the \"RANDOM\" link could have been \"/\", \"/foo-1/\" or \"/foo-3/\")

    (:div.nav
     (:a (@ :href \"/foo-1/\") \"PREV\")
     (:a (@ :href \"/foo-4/\") \"RANDOM\")
     (:a (@ :href \"/foo-3/\") \"NEXT\"))

See `one-default', `one-default-with-toc', `one-default-with-sidebar'
and `one-default-doc'."

  (let* ((pages-not-path
          (seq-remove
           (lambda (page) (string= (plist-get page :one-path) path))
           pages)))
    (when (<= 2 (length pages))
      (let (prev
            (tail pages)
            (random (seq-random-elt pages-not-path)))
        (while (not (string= (plist-get (car tail) :one-path) path))
          (setq prev (car tail))
          (setq tail (cdr tail)))
        `(:div.nav
          ,(when prev `(:a (@ :href ,(plist-get prev :one-path)) "PREV"))
          ,(when (<= 3 (length pages))
             `(:a (@ :href ,(plist-get random :one-path)) "RANDOM"))
          ,(when-let ((next (plist-get (cadr tail) :one-path)))
             `(:a (@ :href ,next) "NEXT")))))))

(defun one-default-list-headlines (tree)
  "Return the list in order of the headlines in TREE.

TREE is meant to be the parsed tree of an org buffer of a website
we want to build.  See `one-parse-buffer'.

Each headline in the returned list is a plist with the following
properties `:id',`:level' and `:title'.

We can use the list returned by `one-default-list-headlines' to build
a table of content of TREE using `one-default-toc'.

See `one-default-with-toc' and `one-default-doc'."
  (org-element-map tree 'headline
    (lambda (elt)
      `(:id ,(org-element-property :one-internal-id elt)
        :level ,(org-element-property :level elt)
        :title ,(org-element-property :raw-value elt)))))

(defun one-default-toc (headlines)
  "Return table of content (TOC) as an HTML string.

The TOC returned is computed from the ordered flat list of
headlines in HEADLINES where the level of each
headline is given by the property `:level'.
See `one-default-list-headlines'.

For instance, evaluating the following form

    (one-default-toc
     \\='((:level 1 :title \"foo\"    :id \"id-foo\")
       (:level 1 :title \"bar-1\"  :id \"id-bar-1\")
       (:level 2 :title \"bar-2\"  :id \"id-bar-2\")
       (:level 3 :title \"bar-3\"  :id \"id-bar-3\")
       (:level 2 :title \"bar-22\" :id \"id-bar-22\")
       (:level 1 :title \"baz\"    :id \"id-baz\")))

returns

     \"
     <ul>
     <li><a href=\\\"#id-foo\\\">foo</a></li>
     <li><a href=\\\"#id-bar-1\\\">bar-1</a>
     <ul>
     <li><a href=\\\"#id-bar-2\\\">bar-2</a>
     <ul>
     <li><a href=\\\"#id-bar-3\\\">bar-3</a></li>
     </ul>
     </li>
     <li><a href=\\\"#id-bar-22\\\">bar-22</a></li>
     </ul>
     </li>
     <li><a href=\\\"#id-baz\\\">baz</a></li>
     </ul>
     \"

See `one-default-with-toc' and `one-default-doc'."

  (let* ((prev-level (1- (plist-get (car headlines) :level)))
	       (start-level prev-level)
         (concat-n-times
          (lambda (n str) (apply #'concat (make-list n str)))))
    (concat
     (mapconcat
      (lambda (headline)
	      (let ((title (plist-get headline :title))
              (href (concat "#" (plist-get headline :id)))
	            (level (plist-get headline :level)))
	        (concat
	         (let* ((delta (- level prev-level))
		              (times (if (> delta 0) (1- delta) (- delta))))
	           (setq prev-level level)
	           (concat
	            (funcall concat-n-times
                       times (cond ((> delta 0) "\n<ul>\n<li>")
			                             ((< delta 0) "</li>\n</ul>\n")))
	            (if (> delta 0) "\n<ul>\n<li>" "</li>\n<li>")))
	         (concat "<a href=\"" href "\">" title "</a>"))))
      headlines "")
     (funcall concat-n-times (- prev-level start-level) "</li>\n</ul>\n"))))

;;; one provide

(provide 'one)
;;; one.el ends here
