1
\$\begingroup\$

I wrote some code to generate list items for a menu in Clojurescript. The intention is that the code takes parsed Markdown (Hiccup) that has been filtered down to h2 and h3 elements, and then converts them into unordered lists where the h3 elements are nested inside their immediate h2 predecessor.

It turned out to be surprisingly difficult in CLJS, and I'm curious if there's a more Lispy and efficient way to do it.

Code

(defn listify-element [element]
  "Replaces element type with :li."
  (vec (concat [:li (last element))]))

(defn listify-headings [headings-list]
  "Takes subitems (in :h2 :h3) and creates sub :uls out of the :h3 lists."
  (vec
   (concat
    [:ul]
    (map-indexed
     (fn [ind headings]
       (if (= 0 (mod ind 2))
         (map listify-element headings)
         (vec (concat [:ul] (map listify-element headings)))))
     (partition-by #(= :h2 (first %)) headings-list)))))

(defn nest-listified-headings [vector-list]
  "Nests sub-:uls inside their preceding :lis."
  (vec (concat [:ul]
          (reduce
           (fn [acc el] (if (= (first el) :ul)
                          (conj (pop (vec acc)) (conj (last acc) el))
                          (concat acc el)))
           vector-list))))

Test case

(nest-listified-headings 
  (listify-headings [[:h2 "Foo"] [:h2 "Bar"] [:h3 "Baz"] [:h3 "Bat"]])

[:ul 
  [:li "Foo"] 
  [:li "Bar" 
  [:ul 
    [:li "Baz"] 
    [:li "Bat"]]]]
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Sorry the late reply, Below is a lispy-er way to get to the same result :)

The main thing is to define a way to get from 2 ul's to a single ul. (so we can use that as a step in a reduce)

basically what we want is, given two ul, to append the one of them to the last item of the other.

The algorithm would be something like this

  • take the last li from the parent.
  • append to it the whole second list.
  • pop it back in the parent

we can make a separate function that does exactly that.

let's call it append-li

(defn append-li 
  "Appends given child to the last element with in the given parrent"
  [child parent]
  (let [ul-without-last-li (vec (butlast parent)) ;; take the parent with out the last element
        last-li (last parent)]) ;;take the last item from the parent
    (conj ul-without-last-li (conj last-li child))) ;; pop it back with the whole second list as a child

Using this append-li function ( could be named better but oh well :P ) we can implement our transformation algorithm like so

  • sort all elements in reverse order of h count (so the larger numbered headers are first (some thing like h3, h2, h1)
  • partition the list of headers by the type of header. so now we have a list of lists of headers, where each list only contains the same kind of headers
  • convert all headers to lis, so that now we have a list of groups of lis and the close to the front the list a group is, the deeper level it has
  • make every group a ul
  • flip the order of the lists (could have been done before), to make it so that the futher away the item
  • reduce with out append-li function.
  • profit :)

All this reversing is a bit annoying, but it ultimately maintains the original order of the items.

(small edit) The reason we want to reverse the thing is that we want to build the tree of ul's in reverse order from the bottom up. We could do it from the top down, but that would require a bit of recursion.

below is the full code with some (hopefully) helpful comments

Full Code:

(defn tag->li 
  "Converts a header tag (or any tag) to a li"
  [[type & content]]
  (into [:li] content))

(defn append-li 
  "Appends given child to the last element with in the given parrent"
  [child parent]
  (let [ul-without-last-li (vec (butlast parent)) 
        last-li (last parent)])
    (conj ul-without-last-li (conj last-li child)))  

(defn listify-headings 
  "Convers a list header tags to a nested unordered list"
  [nodes]
  (let [rev-sorted (sort-by first #(> %2 %1) nodes) ;;sorts by 'h' number, result is a sorted list where all h3 are before h2 etc
        types      (partition-by first rev-sorted)  ;;makes a list of lists, so that first list is only h3, second is only h2 etc...
        lis        (map #(map tag->li %) types)     ;;make the h's to li, we have order now so we don't care about what kind of h's it is, this gives a list of lists with 'lis in them'
        uls        (map #(into [:ul] %) lis)]       ;;raps each makes each list of 'li' to be a 'ul' with li's inside
    (reduce append-li (reverse uls))))              ;;concat each 'ul' to the previous (that's why we reverse) one to produce the tree of uls al lis

P.S.

while both your code and mine work and get the job done, i'm not sure if it's logically ok to do this.

from a semantic standpoint this does not make to much sens, as the desired output means that only the last header on the page has any sub-headers.

basically it would look like all chapters don't have any titles, and the last one has all of them.

i'm not sure whether or not that's the desired thing, but oh well :P

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.