JeremS / prose

An alternate syntax for Clojure inspired by Pollen.
Eclipse Public License 2.0
44 stars 4 forks source link
clojure clojurescript

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")]

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:

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:

Cons:

Sci

Pros:

Cons:

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.