Awesome
Prose
Alternate syntax for Clojure, similar to what Pollen brings to Racket.
Installation
{io.github.jerems/prose {:git/tag "v83", :git/sha "08fe24e3c1"}}
Usage
The main idea is to have programmable documents in Clojure. To do so, Prose flips the relationship between plain text and code. In a clojure file, text is assumed to be code except in special cases like strings and comments. In prose, text is assumed to be just plain text except in special cases i.e. clojure code.
Syntax
Prose provides a reader similar to what we can find in Pollen. Text is
either plain text or a special construct. All special constructs begin with
the character ◊
(lozenge).
Clojure calls:
The text:
We can call ◊(str "code") in text
reads as:
["We can call " (str "code") " in text"]
Clojure symbols:
The text:
We can use symbols ◊|some-symbol
reads as:
["We can use symbols " some-symbol]
Tag function:
The text:
There is a tag function syntax looking like:
◊div[{:class "grid"}]{ some content}
◊div{ some ◊em{content}}
or even:
◊str{text}
reads as:
["There is a tag function syntax looking like:\n" (div {:class "grid"} " some content") "\n" (div " some " (em "content")) "\n\nor even:\n" (str "text")]
- clojure code argument in brackets
- text argument in braces
Escaped / verbatim text:
The text:
The ◊"◊" character.
reads as:
["The " "◊" " character."]
Documents as programs
To get programmable documents prose provides several apis that are meant to work together. We have:
- a reader turning text into code as data
- an API help to evaluate that data using Clojure's eval capabilities
- an API to compile the result of evaluations into the final document
Let's see the whole process in action. We start by requiring the necessary apis and setting up a little helper:
(require '[clojure.java.io :as io])
(require '[clojure.pprint :as pp])
(require '[fr.jeremyschoffen.prose.alpha.reader.core :as reader])
(require '[fr.jeremyschoffen.prose.alpha.eval.common :as eval-common])
(require '[fr.jeremyschoffen.prose.alpha.out.html.compiler :as html-compiler])
(defn display [x]
(with-out-str
(pp/pprint x)))
This is the document we are using for our example:
◊(require '[fr.jeremyschoffen.prose.alpha.out.html.tags :refer [div ul li]])
◊div{
some text
◊ul {
◊li {1}
◊li {2}
}
}
Let's read it:
(def document
(-> "fr/jeremyschoffen/prose/alpha/docs/pages/readme/example-doc.html.prose"
io/resource
slurp
reader/read-from-string))
(display document)
;=>
[(require
'[fr.jeremyschoffen.prose.alpha.out.html.tags :refer [div ul li]])
"\n\n"
(div
"\n some text\n "
(ul "\n " (li "1") "\n " (li "2") "\n ")
"\n")]
Eval it:
(def evaled-document (eval-common/eval-forms-in-temp-ns document))
(display evaled-document)
;=>
[nil
"\n\n"
{:tag :div,
:content
["\n some text\n "
{:tag :ul,
:content
["\n "
{:tag :li, :content ["1"], :type :tag}
"\n "
{:tag :li, :content ["2"], :type :tag}
"\n "],
:type :tag}
"\n"],
:type :tag}]
Compile it to html:
(html-compiler/compile! evaled-document)
;=>
<div>
some text
<ul>
<li>1</li>
<li>2</li>
</ul>
</div>
There are some helpers to make this process easier:
(require '[fr.jeremyschoffen.prose.alpha.document.clojure :as doc])
(defn slurp-doc [path]
(-> path
io/resource
slurp))
(def evaluate (doc/make-evaluator {:slurp-doc slurp-doc
:read-doc reader/read-from-string
:eval-forms eval-common/eval-forms-in-temp-ns}))
(-> "fr/jeremyschoffen/prose/alpha/docs/pages/readme/example-doc.html.prose"
evaluate
html-compiler/compile!)
;=>
<div>
some text
<ul>
<li>1</li>
<li>2</li>
</ul>
</div>
The namespaces fr.jeremyschoffen.prose.alpha.document.*
provide more
functionality than just composing slurp
, read
and eval
functions.
The make-evaluator
functions there sets up the possibility
for documents to import other documents, passing input data to documents...
The ◊ (lozenge) character
One of the first question that came to mind when I discovered Pollen was:
why this ◊
character? I expect the same question will arise for this
project.
Pollen and Prose use ◊
for several reasons. Mainly this character
isn't used as a special character in programming languages. To stick to
Clojure, characters like @
, #
or even &
have special meaning.
◊
not being used either in clojure nor very much in plain text allows
us to have expressions such as:
◊(defn template [v]
◊div { Some value: ◊|v})
In this example there is prose syntax used inside clojure code without
ambiguity. Using the @
as Scribble does would cause problems:
@(defn template [v]
@div { Some value: @|v})
In that case which @
hold Prose's meaning and which are a deref
reader
macro? Using ◊
gets us out of most of these problems. When we want to
use ◊
as text we can use the escaping/verbatim syntax ◊"◊"
.
Also this:
◊(str "◊")
behaves as you'd expect, the ◊ insisde the clojure string isn't special. That should be the extent of our troubles with this character.
For reference here is the answer in the case of pollen from its documentation.
Clojure vs sci evaluation
Currently Prose provides 2 apis to evaluate code. The first one uses Clojure's eval function. The second uses Sci.
There are pros and cons to each approach.
Clojure
Pros:
- An evaluation can use anything that is in the classpath making requiring namespaces easier.
- The api may generally be a bit easier to use.
Cons:
- An evaluation can use anything that is in the classpath which isn't secure.
- I believe clojure doesn't allow code ran outside of its main thread to create / destroy namespaces.
- Porting that functionality to Clojurescript requires going self hosted (eval needs to be there somehow).
Sci
Pros:
- Runs in clojure and clojurescript
- Bringing it's own reifed environment, Sci evaluations can easily happen in several threads.
- Allows us to sandbox what's accessible to the code / document being evaluated.
Cons:
- May be a bit of a perf hit.
- Managing the sci context makes for an api not as easy to use.
Limitations
At the moment using Clojure's shortened syntax for namespace qualified keywords
is a bit tricky to use, it requires knowledge of namespace aliases before
reading documents. The main reader function, using
edamame under the hood accepts
options passed down to edamame allowing this shortened syntax.
(see the docstring of fr.jeremyschoffen.prose.alpha.reader.core/read-from-string
and
edamame's docs)
Mentions
This work is of course inspired and influenced by Pollen and Scribble. The enlive library and ClojureScript are also a big source of inspiration where document compilation is concerned.
License
Copyright © 2020 Jeremy Schoffen.
Distributed under the Eclipse Public License v 2.0.