summaryrefslogtreecommitdiffstats
path: root/site/posts/cleopatra/soupault.org
diff options
context:
space:
mode:
Diffstat (limited to 'site/posts/cleopatra/soupault.org')
-rw-r--r--site/posts/cleopatra/soupault.org702
1 files changed, 702 insertions, 0 deletions
diff --git a/site/posts/cleopatra/soupault.org b/site/posts/cleopatra/soupault.org
new file mode 100644
index 0000000..3fdb8d6
--- /dev/null
+++ b/site/posts/cleopatra/soupault.org
@@ -0,0 +1,702 @@
+#+TITLE: ~soupault~
+
+#+SERIES: ../cleopatra.html
+#+SERIES_PREV: ./theme.html
+#+SERIES_NEXT: ./commands.html
+
+We use ~soupault~ to build this website[fn::~soupault~ is an awesome
+free software project, with a unique approach to static website
+generation. You should definitely [[https://soupault.app][check out their website]]!].
+
+#+begin_export html
+<nav id="generate-toc"></nav>
+#+end_export
+
+* Installation
+
+ We install ~soupault~ in a local switch. We use a witness file
+ ~_opam/.init~ to determine whether or not our switch has always been
+ created during a previous invocation of *~cleopatra~*.
+
+ #+begin_src makefile :tangle soupault.mk
+CONFIGURE += _opam rss.json
+ARTIFACTS += out
+
+soupault-prebuild : _opam/init
+ #+end_src
+
+ Using ~soupault~ is as simple as calling it, without any particular
+ command-line arguments.
+
+ #+begin_src makefile :tangle soupault.mk
+soupault-build : dependencies-prebuild style.min.css
+ @cleopatra echo "Executing" "soupault"
+ @soupault
+ #+end_src
+
+ We now describe our configuration file for ~soupault~.
+
+* Configuration
+
+ #+name: base-dir
+ #+begin_src verbatim :noweb yes :exports none
+~lthms
+ #+end_src
+
+** Global Settings
+
+ The options of the ~[settings]~ section of a ~soupault~
+ configuration are often self-explanatory, and we do not spend too
+ much time to detaul them.
+
+ #+begin_src toml :tangle soupault.conf :noweb yes
+[settings]
+strict = true
+site_dir = "site"
+build_dir = "out/<<base-dir>>"
+doctype = "<!DOCTYPE html>"
+clean_urls = false
+generator_mode = true
+complete_page_selector = "html"
+default_content_selector = "main"
+default_content_action = "append_child"
+page_file_extensions = ["html"]
+ignore_extensions = [
+ "v", "vo", "vok", "vos", "glob",
+ "html~", "org"
+]
+default_template_file = "templates/main.html"
+pretty_print_html = false
+ #+end_src
+
+** Setting Page Title
+
+ We use the “page title” widget to set the title of the webpage
+ based on the first (and hopefully the only) ~<h1>~ tag of the
+ page.
+
+ #+begin_src toml :tangle soupault.conf
+[widgets.page-title]
+widget = "title"
+selector = "h1"
+default = "~lthms"
+prepend = "~lthms: "
+ #+end_src
+
+** Acknowledging ~soupault~
+
+ When creating a new ~soupault~ project (using ~soupault --init~),
+ the default configuration file suggests advertising the use of
+ ~soupault~. Rather than hard-coding the used version of ~soupault~
+ (which is error-prone), we rather determine the version of
+ ~soupault~ with the following script.
+
+ #+NAME: soupault-version
+ #+begin_src bash :results verbatim output
+soupault --version | head -n 1 | tr -d '\n'
+ #+end_src
+
+ The configuration of the widget ---initially provided by
+ ~soupault~--- becomes less subject to the obsolescence[fn::That
+ is, as long as ~soupault~ does not change the output of its
+ ~--version~ option.].
+
+ #+begin_src toml :tangle soupault.conf :noweb yes
+[widgets.generator-meta]
+widget = "insert_html"
+html = """<meta name="generator" content="<<soupault-version()>>">"""
+selector = "head"
+ #+end_src
+
+** Prefixing Internal URLs
+
+ On the one hand, internal links can be absolute, meaning they
+ start with a leading ~/~, and therefore are relative to the
+ website root. On the other hand, website (especially static
+ website) can be placed in larger context. For instance, my
+ personal website lives inside the ~~lthms~ directory of the
+ ~soap.coffee~ domain[fn::To my experience in hosting webapps and
+ websites, this set-up is way harder to get right than I initially
+ expect.].
+
+ The purpose of this plugin is to rewrite internal URLs which are relative to the
+ root, in order to properly prefix them.
+
+ From a high-level perspective, the plugin structure is the following.
+
+ First, we validate the widget configuration.
+
+ #+BEGIN_SRC lua :tangle plugins/urls-rewriting.lua
+prefix_url = config["prefix_url"]
+
+if not prefix_url then
+ Plugin.fail("Missing mandatory field: `prefix_url'")
+end
+
+if not Regex.match(prefix_url, "^/(.*)") then
+ prefix_url = "/" .. prefix_url
+end
+
+if not Regex.match(prefix_url, "(.*)/$") then
+ prefix_url = prefix_url .. "/"
+end
+ #+END_SRC
+
+ Then, we propose a generic function to enumerate and rewrite tags
+ which can have.
+
+ #+BEGIN_SRC lua :tangle plugins/urls-rewriting.lua
+function prefix_urls (links, attr, prefix_url)
+ index, link = next(links)
+
+ while index do
+ href = HTML.get_attribute(link, attr)
+
+ if href then
+ if Regex.match(href, "^/") then
+ href = Regex.replace(href, "^/*", "")
+ href = prefix_url .. href
+ end
+
+ HTML.set_attribute(link, attr, href)
+ end
+ index, link = next(links, index)
+ end
+end
+ #+END_SRC
+
+ Finally, we use this generic function for relevant tags.
+
+ #+BEGIN_SRC lua :tangle plugins/urls-rewriting.lua
+prefix_urls(HTML.select(page, "a"), "href", prefix_url)
+prefix_urls(HTML.select(page, "link"), "href", prefix_url)
+prefix_urls(HTML.select(page, "img"), "src", prefix_url)
+prefix_urls(HTML.select(page, "script"), "src", prefix_url)
+prefix_urls(HTML.select(page, "use"), "href", prefix_url)
+ #+END_SRC
+
+ Again, configuring soupault to use this plugin is relatively
+ straightforward.
+
+ #+BEGIN_SRC toml :tangle soupault.conf :noweb yes
+[widgets.urls-rewriting]
+widget = "urls-rewriting"
+prefix_url = "<<base-dir>>"
+after = "mark-external-urls"
+ #+END_SRC
+
+** Marking External Links
+
+ #+BEGIN_SRC lua :tangle plugins/external-urls.lua
+function mark(name)
+ return '<span class="icon"><svg><use href="/img/icons.svg#'
+ .. name ..
+ '"></use></svg></span>'
+end
+
+links = HTML.select(page, "a")
+
+index, link = next(links)
+
+while index do
+ href = HTML.get_attribute(link, "href")
+
+ if href then
+ if Regex.match(href, "^https?://github.com") then
+ icon = HTML.parse(mark("github"))
+ HTML.append_child(link, icon)
+ elseif Regex.match(href, "^https?://") then
+ icon = HTML.parse(mark("external-link"))
+ HTML.append_child(link, icon)
+ end
+ end
+
+ index, link = next(links, index)
+end
+ #+END_SRC
+
+ #+BEGIN_SRC toml :tangle soupault.conf
+[widgets.mark-external-urls]
+after = "generate-history"
+widget = "external-urls"
+ #+END_SRC
+
+** Generating a Table of Contents
+
+ The ~toc~ widget allows for generating a table of contents for
+ HTML files which contains a node matching a given ~selector~ (in
+ the case of this document, ~#generate-toc~).
+
+ #+begin_src toml :tangle soupault.conf
+[widgets.table-of-contents]
+widget = "toc"
+selector = "#generate-toc"
+action = "replace_content"
+valid_html = true
+min_level = 2
+max_level = 3
+numbered_list = false
+heading_links = true
+heading_link_text = " §"
+heading_links_append = true
+heading_link_class = "anchor-link"
+
+[widgets.append-toc-title]
+widget = "insert_html"
+selector = "#generate-toc"
+action = "prepend_child"
+html = '<h2>Table of Contents</h2>'
+after = "table-of-contents"
+ #+end_src
+
+** Generating Per-File Revisions Tables
+
+*** Users Instructions
+
+ This widgets allows to generate a so-called “revisions table” of
+ the filename contained in a DOM element of id ~history~, based on
+ its history. Paths should be relative to the directory from which
+ you start the build process (typically, the root of your
+ repository). The revisions table notably provides hyperlinks to a
+ ~git~ webview for each commit.
+
+ For instance, considering the following HTML snippet
+
+ #+begin_src html
+<div id="history">
+ site/posts/FooBar.org
+</div>
+ #+end_src
+
+ This plugin will replace the content of this ~<div>~ with the
+ revisions table of ~site/posts/FooBar.org~.
+
+*** Customization
+
+ The base of the URL webview for the document you are currently
+ reading is src_verbatim[:noweb yes :exports code]{<<repo>>}.
+
+ #+name: repo
+ #+begin_src verbatim :exports none
+https://src.soap.coffee/soap.coffee/lthms.git
+ #+end_src
+
+ The template used to generate the revision table is the following.
+
+ #+begin_src html :tangle templates/history.html :noweb yes
+<details id="history">
+ <summary>Revisions</summary>
+ <p>
+ This revisions table has been automatically generated
+ from <a href="<<repo>>">the
+ <code>git</code> history of this website repository</a>, and the
+ change descriptions may not always be as useful as they should.
+ </p>
+
+ <p>
+ You can consult the source of this file in its current version
+ <a href="<<repo>>/tree/{{file}}">here</a>.
+ </p>
+
+ <table class="fullwidth">
+ {{#history}}
+ <tr>
+ <td class="date"
+{{#created}}
+ id="created-at"
+{{/created}}
+{{#modified}}
+ id="modified-at"
+{{/modified}}
+ >{{date}}</td>
+ <td class="subject">{{subject}}</td>
+ <td class="commit">
+ <a href="<<repo>>/commit/{{filename}}/?id={{hash}}">{{abbr_hash}}</a>
+ </td>
+ </tr>
+ {{/history}}
+ </table>
+</details>
+ #+end_src
+
+*** Implementation
+
+ We use the built-in [[https://soupault.neocities.org/reference-manual/#widgets-preprocess-element][=preprocess_element=]] to implement, which
+ means we need a script which gets its input from the standard
+ input, and echoes its output to the standard input.
+
+ #+begin_src toml :tangle soupault.conf
+[widgets.generate-history]
+widget = "preprocess_element"
+selector = "#history"
+command = 'scripts/history.sh templates/history.html'
+action = "replace_element"
+ #+end_src
+
+ This plugin proceeds as follows:
+
+ 1. Using an ad-hoc script, it generates a JSON containing for each revision
+ - The subject, date, hash, and abbreviated hash of the related commit
+ - The name of the file at the time of this commit
+ 2. This JSON is passed to a mustache engine (~haskell-mustache~) with a
+ proper template
+ 3. The content of the selected DOM element is replaced with the output of
+ ~haskell-mustache~
+
+ This translates in Bash like this.
+
+ #+begin_src bash :tangle scripts/history.sh :shebang "#!/usr/bin/bash"
+function main () {
+ local file="${1}"
+ local template="${2}"
+
+ tmp_file=$(mktemp)
+ generate_json ${file} > ${tmp_file}
+ haskell-mustache ${template} ${tmp_file}
+ rm ${tmp_file}
+}
+ #+end_src
+
+ Generating the expected JSON is therefore as simple as:
+
+ - Fetching the logs
+ - Reading 8 line from the logs, parse the filename from the 6th
+ line
+ - Outputing the JSON
+
+ We will use ~git~ to get the information we need. By default,
+ ~git~ subcommands use a pager when its output is likely to be
+ long. This typically includes ~git-log~. To disable this
+ behavior, ~git~ exposes the ~--no-pager~ command. Besides, we
+ also need ~--follow~ and ~--stat~ to deal with file
+ renaming. Without this option, ~git-log~ stops when the file
+ first appears in the repository, even if this “creation” is
+ actually a renaming. Therefore, the ~git~ command line we use to
+ collect our history is
+
+ #+name: gitlog
+ #+begin_src bash :tangle scripts/history.sh :noweb yes
+function gitlog () {
+ local file="${1}"
+ git --no-pager log \
+ --follow \
+ --stat=10000 \
+ --pretty=format:'%s%n%h%n%H%n%cs%n' \
+ "${file}"
+}
+ #+end_src
+
+ This function will generate a sequence of 8 lines containing all
+ the relevant information we are looking for, for each commit,
+ namely:
+
+ - Subject
+ - Abbreviated hash
+ - Full hash
+ - Date
+ - Empty line
+ - Change summary
+ - Shortlog
+ - Empty line
+
+ For instance, the =gitlog= function will output the following
+ lines for the last commit of this very file:
+
+ #+begin_src bash :results verbatim :exports results :noweb yes
+<<gitlog>>
+gitlog "soupault.org" | head -n8
+ #+end_src
+
+ Among other things, the 6th line contains the filename. We need
+ to extract it, and we do that with ~sed~. In case of file
+ renaming, we need to parse something of the form ~both/to/{old =>
+ new}~.
+
+ #+begin_src bash :tangle scripts/history.sh :noweb yes
+function parse_filename () {
+ local line="${1}"
+ local shrink='s/ *\(.*\) \+|.*/\1/'
+ local unfold='s/\(.*\){\(.*\) => \(.*\)}/\1\3/'
+
+ echo ${line} | sed -e "${shrink}" | sed -e "${unfold}"
+}
+ #+end_src
+
+ The next step is to process the logs to generate the expected
+ JSON. We have to deal with the fact that JSON does not allow the
+ last item of an array to be concluded by ",". Besides, we also
+ want to indicate which commit is responsible for the creation of
+ the file. To do that, we use two variables: =idx= and
+ =last_entry=. When =idx= is equal to 0, we know it is the latest
+ commit. When =idx= is equal to =last_entry=, we know we are
+ looking at the oldest commit for that file.
+
+ #+begin_src bash :tangle scripts/history.sh :noweb yes
+function generate_json () {
+ local input="${1}"
+ local logs="$(gitlog ${input})"
+
+ if [ ! $? -eq 0 ]; then
+ exit 1
+ fi
+
+ let "idx=0"
+ let "last_entry=$(echo "${logs}" | wc -l) / 8"
+
+ local subject=""
+ local abbr_hash=""
+ local hash=""
+ local date=""
+ local file=""
+ local created="true"
+ local modified="false"
+
+ echo -n "{"
+ echo -n "\"file\": \"${input}\""
+ echo -n ",\"history\": ["
+
+ while read -r subject; do
+ read -r abbr_hash
+ read -r hash
+ read -r date
+ read -r # empty line
+ read -r file
+ read -r # short log
+ read -r # empty line
+
+ if [ ${idx} -ne 0 ]; then
+ echo -n ","
+ fi
+
+ if [ ${idx} -eq ${last_entry} ]; then
+ created="true"
+ modified="false"
+ else
+ created="false"
+ modified="true"
+ fi
+
+ output_json_entry "${subject}" \
+ "${abbr_hash}" \
+ "${hash}" \
+ "${date}" \
+ "$(parse_filename "${file}")" \
+ "${created}" \
+ "${modified}"
+
+ let idx++
+ done < <(echo "${logs}")
+
+ echo -n "]}"
+}
+ #+end_src
+
+ Generating the JSON object for a given commit is as simple as
+
+ #+begin_src bash :tangle scripts/history.sh :noweb yes
+function output_json_entry () {
+ local subject="${1}"
+ local abbr_hash="${2}"
+ local hash="${3}"
+ local date="${4}"
+ local file="${5}"
+ local created="${6}"
+ local last_entry="${7}"
+
+ echo -n "{\"subject\": \"${subject}\""
+ echo -n ",\"created\":${created}"
+ echo -n ",\"modified\":${modified}"
+ echo -n ",\"abbr_hash\":\"${abbr_hash}\""
+ echo -n ",\"hash\":\"${hash}\""
+ echo -n ",\"date\":\"${date}\""
+ echo -n ",\"filename\":\"${file}\""
+ echo -n "}"
+}
+ #+end_src
+
+ And we are done! We can safely call the =main= function to generate
+ our revisions table.
+
+ #+begin_src bash :tangle scripts/history.sh
+main "$(cat)" "${1}"
+ #+end_src
+
+** Rendering Equations Offline
+ :PROPERTIES:
+ :CUSTOM_ID: katex
+ :END:
+
+*** Users instructions
+
+ Inline equations written in the DOM under the class
+ src_css{.imath} and using the \im \LaTeX \mi syntax can be
+ rendered once and for all by ~soupault~. User For instance,
+ ~<span class="imath">\LaTeX</span>~ is rendered \im \LaTeX \mi as
+ expected.
+
+ Using this widgets requires being able to inject raw HTML in
+ input files.
+
+*** Implementation
+
+ #+begin_src js :tangle scripts/render-equations.js
+var katex = require("katex");
+var fs = require("fs");
+var input = fs.readFileSync(0);
+var displayMode = process.env.DISPLAY != undefined;
+
+var html = katex.renderToString(String.raw`${input}`, {
+ throwOnError : false,
+ displayModed : displayMode
+});
+
+console.log(html)
+ #+end_src
+
+ We reuse once again the =preprocess_element= widget. The selector
+ is ~.imath~ (~i~ stands for inline in this context), and we
+ replace the previous content with the result of our script.
+
+ #+begin_src toml :tangle soupault.conf
+[widgets.inline-math]
+widget = "preprocess_element"
+selector = ".imath"
+command = "node scripts/render-equations.js"
+action = "replace_content"
+
+[widgets.display-math]
+widget = "preprocess_element"
+selector = ".dmath"
+command = "DISPLAY=1 node scripts/render-equations.js"
+action = "replace_content"
+ #+end_src
+
+** RSS Feed
+
+ #+begin_src toml :tangle soupault.conf
+[index]
+index = true
+dump_json = "rss.json"
+extract_after_widgets = ["urls-rewriting"]
+
+[index.fields]
+title = { selector = ["h1"] }
+modified-at = { selector = ["#modified-at"] }
+created-at = { selector = ["#created-at"] }
+ #+end_src
+
+** Series Navigation
+
+ #+begin_src lua :tangle plugins/series.lua
+function get_title_from_path (path)
+ if Sys.is_file(path) then
+ local content_raw = Sys.read_file(path)
+ local content_dom = HTML.parse(content_raw)
+ local title = HTML.select_one(content_dom, "h1")
+
+ if title then
+ return String.trim(HTML.inner_html(title))
+ else
+ Plugin.fail(path .. ' has no <h1> tag')
+ end
+ else
+ Plugin.fail(path .. ' is not a file')
+ end
+end
+ #+end_src
+
+ #+begin_src lua :tangle plugins/series.lua
+function generate_nav_item_from_title (title, url, template)
+ local env = {}
+ env["url"] = url
+ env["title"] = title
+ local new_content = String.render_template(template, env)
+ return HTML.parse(new_content)
+end
+ #+end_src
+
+ #+begin_src lua :tangle plugins/series.lua
+function generate_nav_items (cwd, cls, template)
+ local elements = HTML.select(page, cls)
+
+ local i = 1
+ while elements[i] do
+ local element = elements[i]
+ local url = HTML.strip_tags(element)
+ local path = Sys.join_path(cwd, url)
+ local title_str = get_title_from_path(path)
+
+ HTML.replace_content(
+ element,
+ generate_nav_item_from_title(title_str, url, template)
+ )
+
+ i = i + 1
+ end
+end
+ #+end_src
+
+ #+begin_src lua :tangle plugins/series.lua
+cwd = Sys.dirname(page_file)
+
+home_template = 'This article is part of the series “<a href="{{ url }}">{{ title }}</a>.”'
+nav_template = '<a href="{{ url }}">{{ title }}</a>'
+
+generate_nav_items(cwd, ".series", home_template)
+generate_nav_items(cwd, ".series-prev", nav_template)
+generate_nav_items(cwd, ".series-next", nav_template)
+ #+end_src
+
+#+begin_src toml :tangle soupault.conf
+[widgets.series]
+widget = "series"
+#+end_src
+
+** Injecting Minified CSS
+
+ #+begin_src lua :tangle plugins/css.lua
+style = HTML.select_one(page, "style")
+
+if style then
+ css = HTML.create_text(Sys.read_file("style.min.css"))
+ HTML.replace_content(style, css)
+end
+ #+end_src
+
+ #+begin_src toml :tangle soupault.conf
+[widgets.css]
+widget = "css"
+ #+end_src
+
+** Cleaning-up
+
+ #+begin_src lua :tangle plugins/clean-up.lua
+function remove_if_empty(html)
+ if String.trim(HTML.inner_html(html)) == "" then
+ HTML.delete(html)
+ end
+end
+ #+end_src
+
+ #+begin_src lua :tangle plugins/clean-up.lua
+function remove_all_if_empty(cls)
+ local elements = HTML.select(page, cls)
+
+ local i = 1
+ while elements[i] do
+ local element = elements[i]
+ remove_if_empty(element)
+ i = i + 1
+ end
+end
+ #+end_src
+
+ #+begin_src lua :tangle plugins/clean-up.lua
+remove_all_if_empty("p") -- introduced by org-mode
+remove_all_if_empty("div.code") -- introduced by coqdoc
+ #+end_src
+
+#+begin_src toml :tangle soupault.conf
+[widgets.clean-up]
+widget = "clean-up"
+#+end_src