~subsetpark/bagatto

19aea03fe23fe5486890912df7dc4a936ce617a3 — Zach Smith 4 months ago 075f758 new-demos
Basic site demo
27 files changed, 438 insertions(+), 704 deletions(-)

M bagatto.janet
D demo/bag.sh
A demo/basic-site/README.md
A demo/basic-site/index.janet
A demo/basic-site/pages/about.md
A demo/basic-site/pages/bagatto.md
A demo/basic-site/site-render/css/styles.css
A demo/basic-site/site-render/index.html
A demo/basic-site/site-render/pages/about.html
A demo/basic-site/site-render/pages/bagatto.html
A demo/basic-site/styles.css
R demo/{templates/base_bottom.temple => basic-site/templates/foot.temple}
R demo/{templates/base_top.temple => basic-site/templates/head.temple}
A demo/basic-site/templates/index.temple
A demo/basic-site/templates/page.temple
D demo/config.jdn
D demo/config.json
D demo/helpers.janet
D demo/index.janet
D demo/posts/post.md
D demo/posts/post2.md
D demo/static/hello.png
D demo/templates/post.temple
D demo/templates/posts.temple
D demo/vendor/.cache/https___gitlab.com_louis.jackman_janet-hypertext
D demo/vendor/.manifests/hypertext.jdn
D demo/vendor/hypertext.janet
M bagatto.janet => bagatto.janet +6 -3
@@ 163,7 163,7 @@
  [src attrs]
  (merge-into attrs (jdn-data src)))

(defn parse-json
(defn markdownparse-json
  "Attribute parser for JSON."
  [src attrs]
  (merge-into attrs (json-data src)))


@@ 428,10 428,13 @@
  [template]
  ~(do
     # Write to the template context to track currently rendered template
     (def prev-template ((dyn :cxt) :current-template))
     (put (dyn :cxt) :current-template ,template)
     (let [env (require ,template)
           render-dict ((env 'render-dict) :value)]
       (render-dict args))))
           render-dict ((env 'render-dict) :value)
           out (render-dict args)]
       (put (dyn :cxt) :current-template prev-template)
       out)))

(defn markdown->html
  "Render a markdown string into HTML."

D demo/bag.sh => demo/bag.sh +0 -1
@@ 1,1 0,0 @@
JANET_PATH=vendor ../build/bag index.janet

A demo/basic-site/README.md => demo/basic-site/README.md +45 -0
@@ 0,0 1,45 @@
# Bagatto Demo: A Basic Site

This directory contains everything needed to generate a basic site using Bagatto.

To run the demo, install or build Bagatto (in-depth installation
instructions are available in the Bagatto manual), `cd` into this directory, and then run 

```
bag index.janet
```

You should see Bagatto output its progress as it creates the site. An example output:

```
code-src/bagatto/demo/basic-site [new-demos !] ⊕ bag index.janet
Reading config data spec...
Reading pages data spec...
Reading css data spec...
Beginning 3 jobs...
Loaded config
Loading pages...
[pages] Loading 2 files
Loading css (styles.css)...
Finished jobs.
Reading pages site spec...
Reading index site spec...
Reading css site spec...
Beginning 3 jobs...
Rendering pages...
Rendering index...
Generating path for css...
Finished jobs.
Starting worker pool with 4 workers...
[COPY] site/css/styles.css
[WRITE] site/index.html
[WRITE] site/pages/bagatto.html
[WRITE] site/pages/about.html
Terminated worker pool.
```

The generated HTML will now be available in `site/` and you can open
`site/index.html` in your web browser.

To compare the output, an already-rendered version of the site is
available at `site-render`.

A demo/basic-site/index.janet => demo/basic-site/index.janet +138 -0
@@ 0,0 1,138 @@
### index.janet
###
### A basic Bagatto demo site.
### 
### This Janet module lays out a minimal, but complete, website using
### Bagatto. We'll use it to explore some of the basic functionality
### of the program.

#
# Global Config
#

## `bagatto/set-output-dir!` will set the top level of the directory
## hierarchy that files are created in. This is quite useful, as it
## allows us to use relative directories elsewhere when linking within
## the site.
(bagatto/set-output-dir! "site")

#
# Helper Functions
#

## Define a utility function that takes a file path and returns the
## basename of the file *without* the extension. For instance, it will
## take "foo/bar/baz.md" and return "baz".
##
## This function makes use of the `path` library, as we see in the call
## to `path/basename`. `path/` will be injected into the evaluation
## environment by the Bagatto executable, so we don't need to import
## it.
(defn- basename [path]
  (->> (path/basename path)
       (string/split ".")
       (first)))

# 
# Data Specification
#

### The first value we need to define is `data`, which is the *data
### specification* for our site. It is a simple key-value data
### structure which maps specification names to specifications. Each
### data spec contains an `attrs` and an optional `src` value. If it
### only contains `attrs`, the value at `attrs` will be included
### directly into the site data. If it contains `src`, the value at
### `src` will be evaluated to load one or more *source files*, and
### the value at `attrs` will be treated as a function that's applied
### to each source file, and expected to return the metadata used in
### rendering the site.

## Define a simple *parser function*. Every parser takes two arguments:
## the contents of a file, and the base attributes, and returns the
## same attributes with some additional information added.
##
## This parser uses the `basename` function to add a single `:basename`
## attribute.
(defn parse-page [_contents attrs]
  (let [basename (basename (attrs :path))]
    (put attrs :basename basename)))

(def data {# :config is a literal struct containing some site-wide
           # metadata.
           :config {:attrs {:title "Bagatto Demo"
                            :description "A minimal website"
                            :author "Z. D. Smith"}}
           # :css is a reference to a single file. As the parser, we
           # specify `bagatto/parse-base`, which simply returns the
           # base :path and :contents attributes.
           :css {:src "styles.css"
                 :attrs bagatto/parse-base}
           # :pages is a reference to all the files that match a
           # wildcard. It uses `bagatto/slurp-*`, which is a function
           # which takes a wildcard and will return a new function
           # that will load all the files that match it.
           #
           # `bagatto/*` will simply list all the files (for later
           # copying); `bagatto/slurp-*` will read them all into
           # memory as well.
           #
           # We will use our `parse-page` function defined
           # above as the parser.
           :pages {:src (bagatto/slurp-* "pages/*.md")
                   :attrs parse-page}})

#
# Site Specification
#

## The second value we need to define is `site`, which will be the
## *site specification* for our site. A site specification maps
## specification names to individual specification entries. Each site
## spec will define one or more files to write by giving the path and
## contents of the new files. The names are not used by Bagatto, but
## are useful to the site author for organizational purposes.

## Define a path generator function. A path generator takes two
## arguments: the site data and a single item. It should output the
## path to be written for a file generated from that item.
##
## In this case the single item will be each of the entries in
## `:pages`, above. `page-path` will be a function; it calls
## `bagatto/%p`, which is a convenient way to generate path generator
## functions. `bagatto/%p` provides a simple dsl for defining
## paths. Here we specify a path that looks like this:
## "pages/{{ (slugify (item :basename)) }}.html".
(def page-path (bagatto/%p "pages" '%i :basename '% ".html"))

(def site {# :index is a single file whose path we can specify as a
           # literal string.
           #
           # To specify the contents of the index we call
           # `bagatto/renderer` which returns a *renderer
           # function*. `bagatto/renderer` takes a template path, and
           # optional attributes to merge into the template
           # arguments. It will return a function which will take the
           # site data and output the contents of the index.
           :index {:dest "index.html"
                   :out (bagatto/renderer "templates/index" {:root "./"})}
           # :css uses `:some` to refer directly to an entry in the
           # data specification. It doesn't provide an `:out` value;
           # therefore, Bagatto will just copy the file directly into
           # the path specified at `:dest`.
           :css {:some :css
                 :dest "css/styles.css"}
           # :pages uses `:each` to iterate over all the entries in
           # the data specification `:pages`. Because this
           # specification will output multiple files, `dest` has to
           # be a function, not a literal path value. It will be
           # called in turn on each of the pages and should output a
           # unique path for each one.
           #
           # Similarly, the renderer specified in this entry will be
           # called once for each page, and will output the contents
           # of the generate web page.
           :pages {:each :pages
                   :dest page-path
                   :out (bagatto/renderer "templates/page" {:root "../"})}})


A demo/basic-site/pages/about.md => demo/basic-site/pages/about.md +8 -0
@@ 0,0 1,8 @@
Hopefully, using Bagatto will be a simple, enjoyable experience.

A Static Site Generator is a funny thing. There are [oodles of
them](https://jamstack.org/generators/), but somehow we keep making
new ones. I guess that's because they feel very personal.

Hopefully when you use this program, it feels like a tool that allows
you to directly express your thoughts.

A demo/basic-site/pages/bagatto.md => demo/basic-site/pages/bagatto.md +11 -0
@@ 0,0 1,11 @@
![Il Mago](http://biodhamhlaidh.altervista.org/wp-content/uploads/2016/09/il_mago.png)

# The Bagatto

The *Bagatto* is the 1 of trumps in the Italian tarot (or tarocchi)
deck. The Italian Bagatto became *Pagat* in several central European
languages, including German and Slovenian, where it is still used to
refer to that card in the Tarot/Tarock/Tarokk games of those regions.

It was in this form that it lent its name to the fantastic card game
compendium site, <https://pagat.com>.

A demo/basic-site/site-render/css/styles.css => demo/basic-site/site-render/css/styles.css +45 -0
@@ 0,0 1,45 @@
html {
  font-family: sans-serif;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%; }

body {
  margin: 0; }
  
nav {
  display: block; }

.list {
  list-style-type: none; }

.pl0 {
  padding-left: 0; }

.ma0 {
  margin: 0; }

.dib {
  display: inline-block; }

.pv2 {
  padding-top: .5rem;
  padding-bottom: .5rem; }

.ph3 {
  padding-left: 1rem;
  padding-right: 1rem; }

.fw6 {
  font-weight: 600; }

.nav {
    decoration: none;
    color: #111 !important;
}

.focus-black:focus {
  color: #000; }

.mw6 {
  max-width: 32rem; }


A demo/basic-site/site-render/index.html => demo/basic-site/site-render/index.html +37 -0
@@ 0,0 1,37 @@
<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>Bagatto Demo</title>
  <meta name="description" content="A minimal website">
  <meta name="author" content="Z. D. Smith">
  <link rel="stylesheet" href="./css/styles.css">

</head>

<body>

<h1>Bagatto Demo</h1> 
<div>
  <nav>
    <ul class="list pl0 ma0">
                          <li class="dib mr2">
                    <a class="pv2 ph3 fw6 nav focus-black"
                     href="pages/about.html">about</a>
                    </li>
                    
                    <li class="dib mr2">
                    <a class="pv2 ph3 fw6 nav focus-black"
                     href="pages/bagatto.html">bagatto</a>
                    </li>
                    

    </ul>
  </nav>
</div>
      
</body>
</html>


A demo/basic-site/site-render/pages/about.html => demo/basic-site/site-render/pages/about.html +30 -0
@@ 0,0 1,30 @@
<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>Bagatto Demo</title>
  <meta name="description" content="A minimal website">
  <meta name="author" content="Z. D. Smith">
  <link rel="stylesheet" href="../css/styles.css">

</head>

<body>

<h1>about</h1>
<div class="mw6">
<p>Hopefully, using Bagatto will be a simple, enjoyable experience.</p>

<p>A Static Site Generator is a funny thing. There are <a href="https://jamstack.org/generators/">oodles of
them</a>, but somehow we keep making
new ones. I guess that&#39;s because they feel very personal.</p>

<p>Hopefully when you use this program, it feels like a tool that allows
you to directly express your thoughts.</p>

</div>
</body>
</html>


A demo/basic-site/site-render/pages/bagatto.html => demo/basic-site/site-render/pages/bagatto.html +33 -0
@@ 0,0 1,33 @@
<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">

  <title>Bagatto Demo</title>
  <meta name="description" content="A minimal website">
  <meta name="author" content="Z. D. Smith">
  <link rel="stylesheet" href="../css/styles.css">

</head>

<body>

<h1>bagatto</h1>
<div class="mw6">
<p><img src="http://biodhamhlaidh.altervista.org/wp-content/uploads/2016/09/il_mago.png" alt="Il Mago"></p>

<h1>The Bagatto</h1>

<p>The <em>Bagatto</em> is the 1 of trumps in the Italian tarot (or tarocchi)
deck. The Italian Bagatto became <em>Pagat</em> in several central European
languages, including German and Slovenian, where it is still used to
refer to that card in the Tarot/Tarock/Tarokk games of those regions.</p>

<p>It was in this form that it lent its name to the fantastic card game
compendium site, <a href="https://pagat.com">https://pagat.com</a>.</p>

</div>
</body>
</html>


A demo/basic-site/styles.css => demo/basic-site/styles.css +45 -0
@@ 0,0 1,45 @@
html {
  font-family: sans-serif;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%; }

body {
  margin: 0; }
  
nav {
  display: block; }

.list {
  list-style-type: none; }

.pl0 {
  padding-left: 0; }

.ma0 {
  margin: 0; }

.dib {
  display: inline-block; }

.pv2 {
  padding-top: .5rem;
  padding-bottom: .5rem; }

.ph3 {
  padding-left: 1rem;
  padding-right: 1rem; }

.fw6 {
  font-weight: 600; }

.nav {
    decoration: none;
    color: #111 !important;
}

.focus-black:focus {
  color: #000; }

.mw6 {
  max-width: 32rem; }


R demo/templates/base_bottom.temple => demo/basic-site/templates/foot.temple +1 -5
@@ 1,6 1,2 @@
        </div>
{$ (import hypertext) $}
{% (def dest (path/join (args :root) index-path)) %}
{% (print (hypertext/markup (a :href dest "All posts"))) %}
  </body>
</body>
</html>

R demo/templates/base_top.temple => demo/basic-site/templates/head.temple +13 -9
@@ 1,10 1,14 @@
{% (def title (get-in args [:config :title])) %}
<!DOCTYPE html>
<!doctype html>

<html lang="en">
  <head>
        <meta charset="utf-8">
        <title>{{ title }}</title>
  </head>
  <body>
  <h1>{{ title }}</h1>
        <div class="body">
<head>
  <meta charset="utf-8">

  <title>{{ (get-in args [:config :title]) }}</title>
  <meta name="description" content="{{ (get-in args [:config :description]) }}">
  <meta name="author" content="{{ (get-in args [:config :author]) }}">
  <link rel="stylesheet" href="{{ (args :root) }}css/styles.css">

</head>

<body>

A demo/basic-site/templates/index.temple => demo/basic-site/templates/index.temple +20 -0
@@ 0,0 1,20 @@
{% (bagatto/include "templates/head") %}

<h1>{{ (get-in args [:config :title]) }}</h1> 
<div>
  <nav>
    <ul class="list pl0 ma0">
      {% (each page (args :pages)
            (printf ```
                    <li class="dib mr2">
                    <a class="pv2 ph3 fw6 nav focus-black"
                     href="%s">%s</a>
                    </li>
                    ```
                    (page-path args page)
                    (page :basename))) %}
    </ul>
  </nav>
</div>
      
{% (bagatto/include "templates/foot") %}

A demo/basic-site/templates/page.temple => demo/basic-site/templates/page.temple +6 -0
@@ 0,0 1,6 @@
{% (bagatto/include "templates/head") %}
<h1>{{ (get-in args [:_item :basename]) }}</h1>
<div class="mw6">
{- (bagatto/markdown->html (get-in args [:_item :contents])) -}
</div>
{% (bagatto/include "templates/foot") %}

D demo/config.jdn => demo/config.jdn +0 -1
@@ 1,1 0,0 @@
{:author "Z. D. Smith"}

D demo/config.json => demo/config.json +0 -1
@@ 1,1 0,0 @@
{"subtitle":"A Very Good Blog."}

D demo/helpers.janet => demo/helpers.janet +0 -2
@@ 1,2 0,0 @@
(def rng (math/rng))
(defn int [] (math/rng-int rng))

D demo/index.janet => demo/index.janet +0 -163
@@ 1,163 0,0 @@
### `index.janet`

### A demo index file for Bagatto.

### This simple module demonstrates the basic functionality of the
### Bagatto program, by defining a simple website.

## We can keep our code organized by splitting it out into multiple
## files. We can then import modules from our index file in the
## normal fashion.
(import helpers)

#
# Bagatto API
#

## Bagatto offers the ability to set default values for any of the
## attributes that it expects to find in a data or site specification
## (which we'll see below). Here we set the `attrs` default to
## `jdn-parser`, which means that, in our `data` struct, if we don't
## specify an attrs attribute, Bagatto will assume it refers to a jdn
## file.
(bagatto/set-defaults! {:attrs bagatto/parse-jdn})
(bagatto/set-output-dir! "site")

#
# Data Helpers
#

## A `parse` function is called for every source file that's loaded
## by Bagatto. It takes two arguments: the contents of the file, and
## some pre-generated attributes that are present for every file.
##
## The attributes available by default are:
## * :path - The path of the file from the index
## * :contents - The contents of the file
##
## (Thus providing the source as the first argument is a little
## unnecessary, but slightly more convenient.)
(defn parse-post [src attrs]
  (let [seq (helpers/int)]
    (put attrs :title (string "Blog Post " seq))
    (put attrs :seq seq))
  (bagatto/parse-mmarkdown src attrs))

#
# Data
#

## A Bagatto index module has to define two variables:
## * `data` is a struct mapping data entry *names* to data *specifications*.
## * `site` is a struct describing all of the files that are to be
##   generated, given the input from `data`. Each site specification
##   also has a *name*, though in this case the names are just to make
##   it easier to read by a human author.

# Our `data` struct has several entries:
# * `config` is a static *attrs* struct, containing arbitrary values
#   to be used in rendering the site.
# * `posts` is a specification using `bagatto/slurp-*`, which accepts a
#   file path wildcard and will stream back all the files that match
#   it. Since it will stream multiple files, `post-attrs` is a
#   function which will be called on each file.
# * `static` uses `bagatto/*`, which operates similarly but just
#   returns the file paths that match the wildcard, and doesn't
#   stream the file contents themselves.
# We also have two examples that specify file paths as :src directly,
# and call different attrs functions on them.
(def data {:config {:attrs {:title "A Demo Bagatto Config"}}
           :posts {:src (bagatto/slurp-* "posts/*.md")
                   :attrs parse-post}
           :static {:src (bagatto/* "static/*")
                    :attrs bagatto/parse-base}
           :config-json {:src "config.json"
                         :attrs bagatto/parse-json}
           # NB: We don't specify `:attrs` here; we set a default
           # value above.
           :config-file {:src "config.jdn"}})

#
# Site
#

## To render a new file, Bagatto calls two functions on the attributes
## that were generated in the data step.
##
## The *path* function takes two arguments - the data generated for
## the whole site in the first step, followed by the attributes for
## the specific source file in question. Since data sources can and
## often will refer to multiple files (as we'll see below), these
## functions will be called for each file, and the second argument
## will change each time.
##
## The *path* function should return the file path that the generated
## file should be placed in.

# Here we use the bagatto/%p function to generate a path function for
# us. It exposes a simple DSL and returns a function that will take
# the site data and a post, and return the path for that post.
(def make-post-path (bagatto/%p "posts" '%i :title '% ".html"))

# We can use another generator function, bagatto/path-copier, in the
# case of static paths. This returns a function which will take an
# item and return a new path with the new base (in this case,
# "site/static" followed by the filename of the item.
(def make-static-path (bagatto/path-copier "static"))

# The index file doesn't need a function to determine its path
# (there's only one of them, and it won't depend on the attributes of
# the index), so we can specify it as a simple string.
(def index-path "index.html")

## The *out* function has the same argument signature as the
## *dest* function. It is also called once for each source file. It
## should return the contents of the new file.

# Here we use the bagatto/renderer function to generate a renderer
# function. It returns a function that will take the site data and a
# post and call `bagatto/render` on the specified template, with the
# site and the post as arguments.
(def render-post (bagatto/renderer "templates/post" {:root "../"}))

# The Posts index is generated from the site data and lists out all
# the posts. Therefore, it isn't rendered out of a specific source
# file. We will only call it once, with the site data as the only
# argument, since there is no series of items that are all being
# rendered.
# 
# The bagatto/renderer function will generate functions that can
# accept only one argument---just the site data---without any specific
# item being rendered. The only difference will be that the `:_item`
# attribute won't be available in the template.
(def render-post-index (bagatto/renderer "templates/posts" {:root "./"}))

# Likewise, our `site` struct has three entries---there doesn't have
# to be any clean mapping between entries in `data` and entries in
# `struct`.
#
# * `post-index` is a single file that lists all the posts. Its path
#   is static; its contents are rendered by the `post-index`
#   function, which takes as a single argument the entire output of
#   the data step, and returns the contents of the new file.
# * `posts` is a site specification which will result in multiple
#   files: one for each element of the `posts` site data. To map it
#   back to that site data, we include an additional attribute,
#   `:each`, which refers to an entry found in the site data table.
#
#   Since we've specified an `:each` attribute, both `make-post-path` and
#   `render-post` will take two arguments: the overall site data, as
#   well as the attributes of the specific data entry from `posts`
#   being rendered in that call.
# * `static` includes an `:each` entry, which means it will map over
#   the multiple entries found in `(data :static)`, but no
#   `:contents`. This means that it will simply use `make-static-path`
#   to generate a new path, then copy the file from the original path
#   to the new one.
(def site {:post-index {:dest index-path
                        :out render-post-index}
           :posts {:each :posts
                   :dest make-post-path
                   :out render-post}
           :static {:each :static
                    :dest make-static-path}})

D demo/posts/post.md => demo/posts/post.md +0 -5
@@ 1,5 0,0 @@
## A Post That You Might Be Interested In.

I'm writing this in *Markdown*. Therefore, in my page template, I'll
call `bagatto/markdown->html` in order to render this markdown into
HTML.

D demo/posts/post2.md => demo/posts/post2.md +0 -21
@@ 1,21 0,0 @@
title: A Really Good Title
status: post
---

## The Other Post on My Blog.

This post is very similar to the other post. 

However, it contains footnotes[^1].

[^1]: Bagatto has built-in support for Markdown via the
    [moondown](https://github.com/joy-framework/moondown)
    library. However, moondown doesn't have support for footnotes, so
    we should use multimarkdown to render this instead. Bagatto has
    the `mmarkdown->html` (notice the extra `m`!) function which
    shells out to the multimarkdown application. So if you install
    that, you can take advantage of more features.

    Of course, if you want to use some other markup or text processing
    application, you can write your own function, either in your index
    or a helper module, and use that instead.

D demo/static/hello.png => demo/static/hello.png +0 -0
D demo/templates/post.temple => demo/templates/post.temple +0 -9
@@ 1,9 0,0 @@
{% bagatto/include "templates/base_top" %}
      
      <h1>{{ (get-in args [:_item :title]) }}</h1>
      <p class="post-info">
        {{ (get-in args [:_item :date]) }}
      </p>
      {- (bagatto/mmarkdown->html (get-in args [:_item :contents])) -}

{% (bagatto/include "templates/base_bottom") %}

D demo/templates/posts.temple => demo/templates/posts.temple +0 -43
@@ 1,43 0,0 @@
{% (bagatto/include "templates/base_top") %}

<h2>{{ (get-in args [:config-json "subtitle"]) }}</h2>
<h3>Author: {{ (get-in args [:config-file :author]) }}</h3>
{$
### The posts index.

### We see here the use of functions from three different places:
### 1. the index file that `bag` was called with. In this case that's
###    the `post-path` function. This allows us to use the index module
###    as the source of truth for things like link destinations, rather
###    than having to write them out twice.
### 2. The `bagatto/` namespace. This is the same "standard library"
###    that's made available in the index module and constitutes the
###    loaders, renderers, and utilities provided by the bagatto
###    application.
### 3. Arbitrary, author-managed libraries. In addition to the bagatto
###    library, we've vendored the `hypertext` library in our demo
###    directory (ie, not as a part of bagatto proper). In this case it
###    was installed by `jpm install`, but we could just as easily manage
###    our own `project.janet` file in our site directory.

###    Since temple allows us to run arbitrary Janet code, we can then
###    `import` that library here and then use it to generate HTML,
###    instead of printing out HTML strings directly. `bag` will look for
###    the `JANET_PATH` system environment variable; therefore, if we run
###    `JANET_PATH=vendor bag index.janet` we can access any libraries we
###    ourselves would like to manage. $}

{$ (import hypertext) $}

{% (def img-path (path/join ".." (make-static-path args {:path "static/hello.png"}))) %}
{% (print (hypertext/markup (img :src img-path))) %}

<ul>
{% (loop [post :in (args :posts)]
     (def dest (make-post-path args post))
     (print (hypertext/markup
            (li (a :href dest
                   [(post :title)]))))) %}
</ul>

{% (bagatto/include "templates/base_bottom") %}

D demo/vendor/.cache/https___gitlab.com_louis.jackman_janet-hypertext => demo/vendor/.cache/https___gitlab.com_louis.jackman_janet-hypertext +0 -1
@@ 1,1 0,0 @@
Subproject commit ca43fefc27a1ddc06da2fac4c2b328ca1cd4457d

D demo/vendor/.manifests/hypertext.jdn => demo/vendor/.manifests/hypertext.jdn +0 -1
@@ 1,1 0,0 @@
{:sha "ca43fefc27a1ddc06da2fac4c2b328ca1cd4457d" :paths @["/home/zax/code-src/bagatto/demo/vendor/hypertext.janet"] :repo "https://gitlab.com/louis.jackman/janet-hypertext" :dependencies @[]}

D demo/vendor/hypertext.janet => demo/vendor/hypertext.janet +0 -439
@@ 1,439 0,0 @@
#
# janet-hypertext
#

(defn- nop [])

(defmacro- flip [x]
  ~(set ,x (not ,x)))

(defn- pretty-format []
  (dyn :pretty-format "%q"))

(defn- map-pairs [f xs &keys {:output output}]
  (default output struct)

  (def mapped
    (->> xs
         pairs
         (map (fn [[k v]]
                (f k v)))))

  (output ;(array/concat @[] ;mapped)))

#
# Element Types
#

(def- tag string)
(def- text-node string)

#
# Elements
#

(defn elem
  "Produces a HTML element, where the first argument is a struct of attributes
  and the seconds is a tuple of child elements (which can be strings for text
  nodes). Drop arguments for sane defaults, except for the mandatory `tag`."
  [tag &opt arg rest]
  (unless (symbol? tag)
    (errorf (string "tags for elements must be symbols, e.g. 'p, not "
                    (pretty-format))
            tag))
  (def [attrs children]
    (case (type arg)
      :tuple [{} arg]
      :nil [{} []]
      :struct [arg (if (nil? rest)
                     []
                     rest)]
      (errorf (string "after the tag, the next item must be either a struct of attributes or a tuple of children, not "
                      (pretty-format))
              arg)))
  {:tag tag
   :attrs attrs
   :children children})

#
# Doctypes
#

# Credit to Joy, from which this function was adapted:
# https://github.com/joy-framework/joy/blob/master/src/joy/html.janet
(defn- doctype-string [version &opt style]
  (let [key [version (or style "")]
        doctypes {[:html5 ""] "html"
                  [:html4 :strict] `HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"`
                  [:html4 :transitional] `HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"`
                  [:html4 :frameset] `HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd"`
                  [:xhtml1.0 :strict] `html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"`
                  [:xhtml1.0 :transitional] `html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"`
                  [:xhtml1.0 :frameset] `html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"`
                  [:xhtml1.1 ""] `html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"`
                  [:xhtml1.1 :basic] `html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd"`}
        doctype (doctypes key)]
    (when (nil? doctype)
      (errorf (string "unknown doctype for "
                      (dyn :pretty-format "%q")
                      "; try :html5")
              key))
    doctype))

(defn doctype
  "Produce a doctype with a version such as `:html5` and an _optional_ style
  like `:frameset`"
  [version &opt style]
  {:version version
   :style style})

(defn doctype-to-string [{:version version :style style}]
  (string "<!DOCTYPE " (doctype-string version style) ">"))

#
# Pages: a Doctype and a Root Document Element
#

(defn page
  "Produces a page with a provided doctype and a document root element. If the
  doctype is omitted, HTML5 is assumed."
  [arg1 &opt document]
  (if (nil? document)
    {:doctype (doctype :html5)
     :document arg1}
    (if (struct? arg1)
      {:doctype arg1
       :document document}
      (errorf "expecting a doctype struct; did you type `:html5` where you meant `(hypertext/doctype :html5)`?"))))

(defn- resembles-page [x]
  (def missing (gensym))
  (def finding (get x :doctype missing))
  (not= finding missing))

#
# Escaping
#

(defn- escapes [codes]
  (pairs (map-pairs (fn [char code]
                      [char (string "&" code ";")])
                    codes)))

(defn- escaper [escapes]
  (fn [s]
    (reduce (fn [result [char replacement]]
              (string/replace-all char replacement result))
            s
            escapes)))

(def- text-node-escapes (escapes {"<" "lt"
                                  ">" "gt"
                                  "&" "amp"
                                  "\"" "quot"
                                  "/" "#x2F"
                                  "'" "#x27"
                                  "%" "#37"}))

(def- attr-name-escapes text-node-escapes)

(def- attr-value-escapes (escapes {"\"" "quot"}))

(def- escape-text-node (escaper text-node-escapes))
(def- escape-attr-name (escaper attr-name-escapes))
(def- escape-attr-value (escaper attr-value-escapes))

#
# Marshalling Formatters
#
# Marshalling formatters consist of two actions: indent and newline. They
# implement these according to their overall formatting strategy and take an
# emitter on construction.
#

(defn pretty
  "Format with indents and newlines."
  [emit]
  {:newline (fn (_)
              (emit "\n"))
   :indent (fn (_ indent-level)
             (for _ 0 indent-level
               (emit "  ")))})

(defn no-indents
  "Indent without indents but with newlines."
  [emit]
  {:newline (fn (_)
              (emit "\n"))
   :indent (fn (_ _))})

(defn minified
  "Neither indent nor add newlines."
  [_]
  {:newline (fn (_))
   :indent (fn (_ _))})

(def default-formatter
  "The symbol of the dynamic variable representing the default formatter to use.
  It is used if one is not explicitly passed in to a function."
  (gensym))

(setdyn default-formatter pretty)

#
# Element Marshalling
#

(defn element-marshaller
  "Creates an element marshaller, a function that emits HTML string fragments for an
  element, each string fragment going out via `emit`. No guarantee is made about
  the content or size of the fragments; they are only guaranteed to be a valid
  HTML document if all combined together."
  [emit &keys {:formatter formatter}]
  (default formatter (dyn default-formatter pretty))

  (def formatter (formatter emit))

  (defn quote-attr-value [s]
    (emit "\"")
    (emit (escape-attr-value s))
    (emit "\""))

  (defn attrs-to-str [attrs]
    (loop [[name value] :pairs attrs]
      (emit " ")
      (emit (escape-attr-name name))
      (emit "=")
      (quote-attr-value value)))

  (var elem-to-string nil)

  (defn children-to-str [children indent-level]
    (for i 0 (length children)
      (def child (children i))
      (def previous-child (get children (dec i)))
      (def sequenced-text (and (string? child)
                               (string? previous-child)))
      (def fold-whitespace (or (string? child)
                               (string? previous-child)))

      (when sequenced-text
        (emit " "))

      (unless fold-whitespace
        (:newline formatter)
        (:indent formatter indent-level))

      (case (type child)
        :string (emit (escape-text-node child))
        :struct (elem-to-string child indent-level)
        (errorf (string "expecting either a text node (string) or a child element (struct), but received a "
                        (pretty-format))
                child))))

  (set elem-to-string (fn [elem indent-level &opt top-level]
                        (default top-level false)
                        (emit "<")
                        (emit (elem :tag))
                        (when (elem :attrs)
                          (attrs-to-str (elem :attrs)))
                        (emit ">")
                        (unless (empty? (elem :children))
                          (children-to-str (elem :children) (inc indent-level))
                          (unless (string? (last (elem :children)))
                           (:newline formatter)
                           (:indent formatter indent-level)))
                        (emit "</")
                        (emit (elem :tag))
                        (emit ">")))

  (defn to-string [x]
    (if (string? x)
      x
      (if (resembles-page x)
        (do
          (emit (doctype-to-string (x :doctype)))
          (:newline formatter)
          (elem-to-string (x :document) 0 true))
        (elem-to-string x 0 true))))

  to-string)

#
# Marshalling Producers
#
# Constructors of tables with at least an `:emit` member function. They are used
# to accumulate resulting HTML strings.
#

(defn in-memory-producer
  "Emits string fragments into an in-memory buffer, which can later be
  \"collected\" into a string."
  [&opt buffer]
  (default buffer @"")

  {:emit (fn [_ s]
           (buffer/push-string buffer s))
   :collect (fn [_]
              (string buffer))})

(defn streaming-producer
  "Streams string fragments into the provided function."
  [f]
  {:emit (fn [_ s] (f s))})

(defn to-string
  "Converts an element into a HTML string eagerly in memory, returning a
  string."
  [elem &keys {:formatter formatter}]
  (let [producer (in-memory-producer)
        emit (fn [s]
               (:emit producer s))
        marshal-element (element-marshaller emit
                                            :formatter formatter)]
    (marshal-element elem)
    (:collect producer)))

(defn emit-as-string-fragments
  "Converts an element into a HTML string lazily, streaming the string fragments
  out via the provided function."
  [elem emit &keys {:formatter formatter}]
  (let [producer (streaming-producer emit)
        emit (fn [s]
               (:emit producer s))
        marshal-element (element-marshaller emit
                                            :formatter formatter)]
    (marshal-element elem)))

#
# DSL Constructors
#
# Using `elem` is a bit raw. Provide a data-oriented wrapper around it,
# and provide a macro wrapper around that. Each one trades off more flexibility
# for succinctness.
#

(var from-data
  "Turns a data representation of elements into an in-memory element. See
  `README.md` for an example of the data's structure."
  nil)

(defn- html-from-tuple [t]
  (when (empty? t)
    (error "an empty tuple isn't enough to describe a HTML element; either use a standalone symbol, or a tuple with at least two elements for one including attributes and/or children"))
  (when (= 1 (length t))
    (error "for elements without attributes and children, just use them standalone outside of a tuple"))
  (if (< 3 (length t))
    (error "a HTML tuple can have a maximum of three items: a tag, an attributes struct, and a children tuple; did you forget the wrap all of the children nodes in `[` and `]`, or forget to put the attributes straight after the tag?"))

  (def [tag arg rest] t)
  (def [attrs children]
    (case (type arg)
      :tuple [{} arg]
      :struct [arg (if (nil? rest)
                     []
                     rest)]
      [{} (tuple/slice t 1)]))
  (def converted-attrs @{})

  # Autoconvert attribute names into keywords.
  (eachp [name value] attrs
    (set (converted-attrs (keyword name))
         (string value)))

  (elem tag
        (table/to-struct converted-attrs)
        (tuple/slice (map from-data children))))

(set from-data (fn [data]
                 (case (type data)
                   :symbol (elem data)
                   :tuple (html-from-tuple data)
                   :struct data

                   # Autoconvert Janet values into strings for text nodes.
                   (string data))))

(defn- html-attrs [args]
  (def result @{})
  (var on-key true)
  (var pending-key nil)
  (var rest [])
  (for i 0 (length args)
    (def arg (args i))
    (if on-key
      (if (keyword? arg)
        (set pending-key arg)
        (do
          (set rest (tuple/slice args i))
          (break)))
      (set (result pending-key)
           (if (symbol? arg)
             ['unquote arg]
             arg)))
    (flip on-key))
  (def attrs (table/to-struct result))
  [attrs rest])

(defn- html-body [body]
  (if (tuple? body)
    (if (= (tuple/type body) :brackets)
      (do
        (unless (= (length body)
                   1)
          (errorf (string "escaped Janet values wrapped in `[` and `]` within hypertext templates can only contain one value, not "
                          (dyn :pretty-format "%q"))
                  body))
        ['unquote (body 0)])
      (let [[tag arg] body
            rest (tuple/slice body 1)
            [attrs children] (if (keyword? arg)
                               (html-attrs rest)
                               [{} rest])
            transformed-children (tuple/slice (map html-body children))]
        [tag attrs transformed-children]))
    body))

(defn- from-gen [body]

  (if (< 3 (length body))
    (error "up to 3 elements can be provided: a doctype, a doctype variant, and a document, and only the document element is mandatory"))

  (if (and (< 1 (length body))
           (not (keyword? (body 0))))
    (error "only a single root element can be passed to `hypertext/markup`; if you want to specify doctypes, ensure keywords are being used and that they come before the root document element"))

  (let [[first second rest] body]
    (if (keyword? first)
      (let [[version style document] (if (keyword? second)
                                       [first
                                        second
                                        rest]
                                       [first
                                        nil
                                        second])]
        (def data (html-body document))
        {:doctype (doctype version style)
         :document ~(,from-data (,'quasiquote ,data))})
      (do
        (def data (html-body first))
        ~(,from-data (,'quasiquote ,data))))))

(defmacro from
  "Produces a HTML element or a whole page from a lightweight representation
  based on Janet syntax. Whether it's an element or a whole page depends on
  whether it starts with doctype-related keywords. See README.md for an
  example."
  [& body]
  (from-gen body))

(defmacro markup
  "Produces a HTML string or a whole page string from a lightweight
  representation based on Janet syntax. Whether it's an element or a whole page
  depends on whether it starts with doctype-related keywords. See README.md for
  an example. There is no formatter argument; formatting can only be changed by
  setting `hypertext/default-formatter`."
  [& body]

  ~(,to-string ,(from-gen body)))