a592bd3c0b0a88bf6a3fb283e2d3cd13bc301417 — Zach Smith 10 months ago de4debe
Split into two phases: data and site
4 files changed, 66 insertions(+), 98 deletions(-)

M bagatto.janet
M demo/index.janet
M demo/templates/post.temple
M main.janet
M bagatto.janet => bagatto.janet +1 -1
@@ 12,7 12,7 @@
     (fn [] 
       (loop [file :in filenames]
         (yield (slurp file)))))))
         (yield [file (slurp file)]))))))

(defn slugify
  "Normalize a string for use as a slug."

M demo/index.janet => demo/index.janet +25 -14
@@ 1,27 1,38 @@
(import helpers)

(defn post-attrs [src]
# Data Helpers

(defn post-attrs [_src attrs]
  (let [seq (helpers/int)]
    {:seq seq
     :post src
     :title (string "Blog Post " seq)}))
    (put attrs :title (string "Blog Post " seq))
    (put attrs :seq seq)))

# Content Helpers

(defn post-path [config attrs]
(defn post-path [_site attrs]
  (string "site/posts/" (bagatto/slugify (attrs :title)) ".html"))

(defn post-content [config attrs]
  (bagatto/render "templates/post" config attrs))
(defn post-content [site attrs]
  (bagatto/render "templates/post" site attrs))

(defn post-index [site]
  (bagatto/render "templates/posts" site))

(defn post-index [config {:posts posts}]
  (bagatto/render "templates/posts" config))
# Bagatto API

(def config {:blog-title "A Demo Bagatto Config"})
(def data {:config {:attrs {:blog-title "A Demo Bagatto Config"}}
           :posts {:src (bagatto/* "posts/*.md")
                   :attrs post-attrs}})

(def site
  {:post-index {:path "site/index.html"
                :contents post-index
                :requires :posts}
   :posts {:src (bagatto/* "posts/*.md")
           :attrs post-attrs
                :contents post-index}
   :posts {:each :posts
           :path post-path
           :contents post-content}})

M demo/templates/post.temple => demo/templates/post.temple +1 -1
@@ 5,7 5,7 @@
      <p class="post-info">
        {{ (get-in args [:_item :date]) }}
      {- (bagatto/mmarkdown->html (get-in args [:_item :post])) -}
      {- (bagatto/mmarkdown->html (get-in args [:_item :src])) -}

{$ (import ./base_bottom :as base_bottom) $}
{% (base_bottom/render-dict args) %}

M main.janet => main.janet +39 -82
@@ 37,97 37,54 @@
    (f ;args)

(defn produce-writers [site config]
(defn load-data [data-spec]
  Main business logic: given a site definition and a global config,
  produce a sequence of fibers that represent all of the files to be
  generated for this site.
  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
  (let [res @{}]
    (loop [[entry spec] :pairs data-spec]
      (put res entry (match spec
                       {:src loader :attrs attrs-f}
                       (seq [[filename file-contents] :generate loader]
                            (->> @{:path filename :src file-contents}
                                 (attrs-f file-contents)))
                       {:attrs attrs}

  This function handles the definitions contained in `site` one by
  one. Site definition entries can declare dependencies on each other,
  so we have a very naive implementation of dependency management
  where we follow the dependency tree and eagerly attempt to generate
  writers, tracking which entries have been handled by populating
(defn produce-writers [site data]
  (let [pending (gensym)
        site-plan (table ;(mapcat |[$0 pending] (keys site)))
        writers @[]]
    (defn get-plans [req-or-reqs]
      Return the generated page attributes (or the absence thereof)
      for one or many entry dependencies.
        (nil? req-or-reqs) @{}
        (tuple? req-or-reqs) (table ;(mapcat |[$0 (site-plan $0)] req-or-reqs))
        true @{req-or-reqs (site-plan req-or-reqs)}))
    (defn write-plan [name entry]
      Given a site entry whose requirements have all been populated
      (or that has none), generate writer fiber for all of its output
      files and write to the site-plan with all the page attributes.
      (def plans @[])
      (def required (get-plans (entry :requires)))
      (match entry
        # Handle generation of files from user-managed source files
        # (eg, render markdown files into HTML
        {:src filenames
         :attrs attr-f
  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
  (let [writers @[]]
    (loop [[_entry spec] :pairs site]
      (match spec
        {:each site-selector
         :path path-f
         :contents content-f}
          (loop [file :generate filenames]
            (let [attrs (maybe-apply attr-f [file])
                  args (case (length required)
                         0 [config attrs]
                         [config attrs required])
                  path (maybe-apply path-f args)
                  contents (maybe-apply content-f args)
                  writer (new-writer path contents)]
              (array/push plans attrs)
              (array/push writers writer)))
          (put site-plan name plans))
        # Handle generation of "synthetic" files that are built from
        # templates or other functions, but have no user input files
        {:path path-f :contents content-f}
        (let [args (case (length required)
                     0 [config]
                     [config required])
              path (maybe-apply path-f args)
              contents (maybe-apply content-f args)
              writer (new-writer path contents)]
          (array/push plans :ok)
         :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))))
    (defn handle-entry [[name entry]]
      Attempt to populate the site plan and generate writers for a
      site entry. If necessary, force all of its dependencies first.
      (->> (entry :requires)
           (filter |(= ($0 1) pending))
           (map |[($0 0) (site ($0 0))])
           (map handle-entry))
      (write-plan name entry))
    # Trigger population of the site plan.
    (loop [[name entry] :pairs site]
      (if (= pending (site-plan name))
        (handle-entry [name entry])))

(defn main [& [_ index]]
  (let [env (load-file index)
        data-spec ((env 'data) :value)
        data (load-data data-spec)
        site ((env 'site) :value)
        config ((env 'config) :value)
        writers (produce-writers site config)]
        writers (produce-writers site data)]
    (each writer writers (resume writer))))