Alternate syntax for Clojure, similar to what Pollen brings to Racket.
{io.github.jerems/prose {:git/tag "v83", :git/sha "08fe24e3c1"}}
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.
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).
The text:
We can call ◊(str "code") in text
reads as:
["We can call " (str "code") " in text"]
The text:
We can use symbols ◊|some-symbol
reads as:
["We can use symbols " some-symbol]
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")]
The text:
The ◊"◊" character.
reads as:
["The " "◊" " character."]
To get programmable documents prose provides several apis that are meant to work together. We have:
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...
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.
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.
Pros:
Cons:
Pros:
Cons:
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)
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.
Copyright © 2020 Jeremy Schoffen.
Distributed under the Eclipse Public License v 2.0.