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'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 => +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)))
-