~subsetpark/bagatto

976bb0ac48e82d932c6a3f084c399e6c8e808531 — Zach Smith 10 months ago d613e64
Add debug and repl functions
8 files changed, 200 insertions(+), 127 deletions(-)

M bagatto.janet
M demo/index.janet
M demo/templates/base_bottom.temple
M demo/templates/posts.temple
M main.janet
M project.janet
A src/core.janet
R util.janet => src/util.janet
M bagatto.janet => bagatto.janet +51 -17
@@ 6,6 6,7 @@
(import json)

(import src/multimarkdown)
(import src/core)

(defn set-defaults!
  ``


@@ 29,6 30,8 @@
  [attribute]
  (-> (dyn :bagatto-defaults) (in attribute)))

(defn debug! [keys] (setdyn :bagatto-debug keys))

#
# DATA
#


@@ 64,14 67,15 @@
(defn json-attrs [src attrs] (merge-into attrs (json-data src)))

(defn- adjust-for-display [date]
  (-> (mapcat (fn [[k v]] (if (and (number? v) (find |(= $0 k) [:month-day :month :year-day :week-day]))
                          [k (inc v)]
                          [k v]))
             (pairs date))
  (-> (fn [[k v]] (if (and (number? v) (index-of k [:month-day :month :year-day :week-day]))
                  [k (inc v)]
                  [k v]))
     (mapcat (pairs date))
     (splice)
     (struct)))

(defn parse-date
(defn datestr->secs [datestr] (-> (sh/$< date -d ,datestr "+%s") (string/trim) (parse)))
(defn datestr->date
  ```
  Given a string representation of a date or datetime, return a struct
  of the kind returned by `os/date`. Pass a truthy value in the second


@@ 79,16 83,21 @@
  month to be 1-indexed).
  ```
  [datestr &opt for-display?]
  (let [parsed (-> (sh/$< date -d ,datestr "+%s")
                  (string/trim)
                  (parse)
  (let [parsed (-> datestr
                  (datestr->secs)
                  (os/date))]
    (if for-display? (adjust-for-display parsed) parsed)))

(defn eval-data [data] (core/load-data data))
#
# SITE
#

(defn- debug [template attrs keys-to-debug]
  (print "Template: " template "\nArgs:")
  (pp (keys attrs))
  (each key keys-to-debug
    (print key ":")
    (pp (attrs key))))

(defn render
  "Merge attributes and call a template's render-dict function."


@@ 97,10 106,27 @@
        env (require template)
        render-dict ((env 'render-dict) :value)
        attrs (merge site {:_item item})]
    
    (match (dyn :bagatto-debug)
      nil :ok
      keys-to-debug (debug template attrs keys-to-debug))
    
    (with-dyns [:out res]
      (render-dict attrs))
    res))

(defn renderer [template-path]
  (fn [site &opt item]
    (if item
      (render template-path site item)
      (render template-path site))))

(defn eval-site [site data] (core/produce-writer-specs site data))

(defn write-site [writer-specs]
  (-> writer-specs
     (core/produce-writers)
     (core/resume-writers)))
#
# TEMPLATE
#


@@ 110,11 136,14 @@
  [s]
  (def not-alpha (comptime (peg/compile '(if-not (+ :w "-") 1))))
  (def space-underscore (comptime (peg/compile '(set " _"))))
  (->> s
       (string/trim)
       (string/ascii-lower)
       (peg/replace-all space-underscore "-")
       (peg/replace-all not-alpha "")))
  (if s
    (->> s
         (string/trim)
         (string/ascii-lower)
         (peg/replace-all space-underscore "-")
         (peg/replace-all not-alpha "")
         (string))
    ""))

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


@@ 124,6 153,11 @@
  "Render a markdown string using multimarkdown."
  [md] (multimarkdown/snippet md))

(defn shift-up
  "Given a path to a file, return a new path that goes up one then follows the same path."
  [path] (path/join ".." path))
(defn format [templ & xs]
  ```
  A simple wrapper around `string/format` to ease development. If one
  of `xs` is nil, it will output an empty string rather than crashing
  the template.
  ```
  (let [escaped (map |(or $0 "") xs)]
    (string/format templ ;escaped)))

M demo/index.janet => demo/index.janet +12 -9
@@ 62,14 62,6 @@
(defn post-content [site post]
  (bagatto/render "templates/post" site post))

## 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.
(defn post-index [site]
  (bagatto/render "templates/posts" site))

#
# Bagatto API
#


@@ 123,7 115,18 @@
##   being rendered in that call.
(def site
  {:post-index {:path index-path
                :contents post-index}
                # 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.
                #
                # Here we use the `bagatto/renderer` convenience
                # function, which creates a function which receives
                # one or two arguments and then calls `bagatto/render`
                # with the given template.
                :contents (bagatto/renderer "templates/posts")}
   :posts {:each :posts
           :path post-path
           :contents post-content}})

M demo/templates/base_bottom.temple => demo/templates/base_bottom.temple +1 -1
@@ 1,6 1,6 @@
        </div>
{$ (import hypertext) $}
{$ (def dest (bagatto/shift-up index-path)) $}
{$ (def dest index-path) $}
{% (print (hypertext/markup (li (a :href dest "All posts")))) %}
  </body>
</html>

M demo/templates/posts.temple => demo/templates/posts.temple +1 -1
@@ 31,7 31,7 @@
{$ (import hypertext) $}
<ul>
{% (loop [post :in (args :posts)]
     (def dest (-> (post-path args post) (bagatto/shift-up)))
     (def dest (-> (post-path args post)))
     (print (hypertext/markup
            (li (a :href dest
                   [(post :title)]))))) %}

M main.janet => main.janet +27 -98
@@ 1,7 1,7 @@
(import temple)
(import path)
(import util)
(import argparse :prefix "")

(import src/core)

(def bagatto
  ```


@@ 25,108 25,37 @@
  (temple/add-loader)
  (dofile index :env bagatto))

(defn- new-writer [path contents]
  (fiber/new
   (fn []
     (print path)
     (let [ppath (path/dirname path)]
       (util/mkpath ppath)
       (spit path contents)))))

(defn- maybe-apply [f args]
  (if (function? f)
    (f ;args)
    f))

(defn- struct->table [s]
  (->> s (kvs) (splice) (table)))
(def argparse-params ["A transparent, extensible static site generator."
                      "repl" {:kind :flag :help "Compile your index module and enter the REPL."}
                      :default {:kind :option :help "The index module to evaluate." :required true} ])

(defn- set-defaults [spec defaults]
  (table/setproto (struct->table spec) (struct->table defaults)))

(defn load-data [data-spec defaults]
  ```
  First phase of main business logic. `data-spec` contains a
  specification of all the sources necessary to generate our `attrs`
  entries, which are the data structures to be used in generating the
  site.

  A data specification can be in one of four formats:
(defn main [& args]
  
  1. A simple struct with only an :attrs field. The attrs generated
  will simply be the value at `:attrs`.
  2. A struct with a :src that's a string path to a file relative from
  the current directory, and an :attrs function that will be called on
  the contents of the file.
  3. A struct with a :src that's a 0-arity function that will return a
  [filename file-contents] tuple, and an :attrs function that will be
  called on the file-contents.
  4. A struct with a :src that returns a fiber which will yield some
  finite number of [filename file-contents] tuples, and an :attrs
  function that will be called on each file-contents.
  ```
  (defn- make-attrs [filename file-contents attrs-f]
    (->> @{:path filename :src file-contents}
         (attrs-f file-contents)))
  
  (let [res @{}]
    (loop [[entry spec] :pairs data-spec]
      (let [with-defaults (set-defaults spec defaults)]
        (put res entry (match with-defaults
                         
                         ({:src loader :attrs attrs-f} (fiber? loader))
                         (seq [[filename file-contents] :generate loader]
                              (make-attrs filename file-contents attrs-f))
                         
                         ({:src loader :attrs attrs-f} (function? loader))
                         (let [[filename file-contents] (loader)]
                           (make-attrs filename file-contents attrs-f))
                         
                         ({:src path :attrs attrs-f} (string? path))
                         (let [file-contents (slurp path)]
                           (make-attrs path file-contents attrs-f))
                         
                         {:attrs attrs}
                         attrs))))
    res))

(defn produce-writers [site data defaults]
  ``
  Second phase of main business logic. `site` contains a specification
  for generating a website and `data` is all the source data we have to
  do it with. Here we generate a new writer fiber for each file in the
  website.
  ``
  (let [writers @[]]
    (loop [[_entry spec] :pairs site]
      (let [with-defaults (set-defaults spec defaults)]
        (match with-defaults
          {:each site-selector
           :path path-f
           :contents contents-f}
          (loop [item :in (data site-selector)]
            (let [path (maybe-apply path-f [data item])
                  contents (maybe-apply contents-f [data item])
                  writer (new-writer path contents)]
              (array/push writers writer)))
          {:path path-f :contents contents-f}
          (let [path (maybe-apply path-f [data])
                contents (maybe-apply contents-f [data])
                writer (new-writer path contents)]
            (array/push writers writer)))))
    writers))

(defn main [& [_ index]]
  (match (os/getenv "JANET_PATH")
    nil :ok
    janet-path (put root-env :syspath janet-path))

  (let [env (load-file index)
        _ (merge-into temple/base-env env)
        defaults (env :bagatto-defaults)
        data-spec ((env 'data) :value)
        data (load-data data-spec defaults)
        site ((env 'site) :value)
        writers (produce-writers site data defaults)]
  (let [args (argparse ;argparse-params)
        index (args :default)
        env (load-file index)]
    (merge-into temple/base-env env)

    (if (args "repl")
      # REPL mode: Enter a REPL to experiment with the contents of the
      # index module.
      (repl nil nil env)
      # Normal mode: evaluate index module and write site.
      (do
        (defn value [sym] ((env sym) :value))
    
    (each writer writers (resume writer))))
        (setdyn :bagatto-debug (env :bagatto-debug))
        (setdyn :bagatto-defaults (env :bagatto-defaults))

        (let [data-spec (value 'data)
              data (core/load-data data-spec)
              site (value 'site)
              writer-specs (core/produce-writer-specs site data)
              writers (core/produce-writers writer-specs)]
          (core/resume-writers writers))))))

M project.janet => project.janet +2 -1
@@ 6,7 6,8 @@
                 {:repo "https://github.com/joy-framework/moondown" :tag "0.2.0"}
                 "https://github.com/janet-lang/path.git"
                 "https://github.com/andrewchambers/janet-jdn.git"
                 "https://github.com/janet-lang/json.git"])
                 "https://github.com/janet-lang/json.git"
                 "https://github.com/janet-lang/argparse.git"])

(declare-executable
 :name "bag"

A src/core.janet => src/core.janet +106 -0
@@ 0,0 1,106 @@
(import path)

(import src/util)

(defn- struct->table [s]
  (->> (or s @{}) (kvs) (splice) (table)))

(defn- maybe-apply [f args]
  (if (function? f)
    (f ;args)
    f))



(defn- set-defaults [spec]
  (table/setproto (struct->table spec)
                  (struct->table (dyn :bagatto-defaults))))

(defn load-data [data-spec]
  ```
  First phase of main business logic. `data-spec` contains a
  specification of all the sources necessary to generate our `attrs`
  entries, which are the data structures to be used in generating the
  site.

  A data specification can be in one of four formats:
  
  1. A simple struct with only an :attrs field. The attrs generated
  will simply be the value at `:attrs`.
  2. A struct with a :src that's a string path to a file relative from
  the current directory, and an :attrs function that will be called on
  the contents of the file.
  3. A struct with a :src that's a 0-arity function that will return a
  [filename file-contents] tuple, and an :attrs function that will be
  called on the file-contents.
  4. A struct with a :src that returns a fiber which will yield some
  finite number of [filename file-contents] tuples, and an :attrs
  function that will be called on each file-contents.
  ```
  (def res @{})
  (defn make-attrs [filename file-contents attrs-f]
    (->> @{:path filename :src file-contents}
         (attrs-f file-contents)))
  (loop [[entry spec] :pairs data-spec]
    (let [with-defaults (set-defaults spec)
          transform-f (or (spec :transform) identity)]
      (put res entry (match with-defaults
                       
                       ({:src loader :attrs attrs-f} (fiber? loader))
                       (-> (seq [[filename file-contents] :generate loader]
                               (make-attrs filename file-contents attrs-f))
                          (transform-f))
                       
                       ({:src loader :attrs attrs-f} (function? loader))
                       (let [[filename file-contents] (loader)]
                         (make-attrs filename file-contents attrs-f))
                       
                       ({:src path :attrs attrs-f} (string? path))
                       (let [file-contents (slurp path)]
                         (make-attrs path file-contents attrs-f))
                       
                       {:attrs attrs}
                       attrs))))
  res)

(defn produce-writer-specs [site data]
  ``
  Second phase of main business logic. `site` contains a specification
  for generating a website and `data` is all the source data we have to
  do it with. Here we generate a new writer fiber for each file in the
  website.
  ``
  (def writers @[])
  (defn push-writer [path contents]
    (array/push writers [path contents]))
  (loop [[_entry spec] :pairs site]
    (let [with-defaults (set-defaults spec)]
      (match with-defaults
        
        {:each site-selector
         :path path-f
         :contents contents-f}
        (loop [item :in (data site-selector)]
          (let [path (maybe-apply path-f [data item])
                contents (maybe-apply contents-f [data item])]
            (push-writer path contents)))
        
        {:path path-f
         :contents contents-f}
        (let [path (maybe-apply path-f [data])
              contents (maybe-apply contents-f [data])]
          (push-writer path contents)))))
  
  writers)

(defn- new-writer [path contents]
  (fiber/new
   (fn []
     (print path)
     (let [ppath (path/dirname path)]
       (util/mkpath ppath)
       (spit path contents)))))

(defn produce-writers [specs] (seq [spec :in specs] (new-writer ;spec)))

(defn resume-writers [writers] (each writer writers (resume writer)))

R util.janet => src/util.janet +0 -0