summaryrefslogtreecommitdiff
path: root/src/clarktown/parsers/list_block.clj
blob: 40087af67b7622eae96c3ebf3940a2866d902fab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
(ns clarktown.parsers.list-block
  (:require
    [clojure.string :as string]))


(defn is?
  "Determines whether we're dealing with a list block or not."
  [block]
  (->> (string/trim block)
       (re-matches #"(?s)^(\d\.|\*).*$")))


(defn string->indent-n
  "Returns the indentation count from left of `str`, which must be
  in spaces and not tabs."
  [str]
  (count (take-while #{\space} str)))


(defn compose-items-with-indent-guides
  "Composes a vector of maps from given `block` that adds a unique
  ID to each line as well as its `indent-n` which is used later
  on to determine hierarchies. "
  [block]
  (->> (string/split-lines block)
       (mapv
         (fn [line]
           {:id (random-uuid)
            :indent-n (string->indent-n line)
            :value (-> line
                       string/trim)}))))


(defn find-parent-id
  "Assuming a 1-level `items`, will attempt to find the parent `id`
  of the item at given `index`. Will return `nil` otherwise."
  [items index]
  (let [indent-n-at-index (:indent-n (nth items index))]
    (-> (->> (split-at index items)
             first
             reverse
             (remove #(or (> (:indent-n %) indent-n-at-index)
                          (= (:indent-n %) indent-n-at-index)))
             first
             :id))))


(defn compose-items-with-parents
  "Composes a 1-level list of items from `block` and adds parent
  information to each if they belong to another item. The result
  of this is used to build the final data tree."
  [block]
  (let [items (compose-items-with-indent-guides block)]
    (->> items
         (map-indexed
           (fn [index line]
             (merge line {:parent (find-parent-id items index)}))))))


(defn add-to-parent
  "Recursively scans `items`, which can be multiple levels deep,
  and tries to find a home for `item` according to its parent ID."
  [items item]
  (->> items
       (mapv
         (fn [i]
           (if (= (:id i) (:parent item))
             (if (:items i)
               (assoc i :items (concat (:items i) [item]))
               (assoc i :items [item]))
             (if (:items i)
               (assoc i :items (add-to-parent (:items i) item))
               i))))))


(defn compose-item-tree
  "Given a `block`, composes a data representation of it based on
  the indentation of each line."
  [block]
  (loop [result []
         items (compose-items-with-parents block)]
    (if (empty? items)
      result
      (let [item (first items)
            parent (:parent item)
            new-item {:id (:id item)
                      :value (:value item)}]
        (recur (if parent
                 (add-to-parent result item)
                 (concat result [new-item]))
               (drop 1 items))))))


(defn render-items
  "Renders an ordered/un-ordered list hierarchy from given `items`."
  [items]
  (loop [result ""
         inner-items items]
    (if (empty? inner-items)
      (if (string/starts-with? (:value (first items)) "*")
        (str "<ul>" result "</ul>")
        (str "<ol>" result "</ol>"))
      (let [inner-item (first inner-items)
            value (if (string/starts-with? (:value inner-item) "*")
                    (-> (string/replace-first (:value inner-item) "*" "")
                        string/trim)
                    (-> (string/replace-first (:value inner-item) #"\d\." "")
                        string/trim))]
        (recur (if (:items inner-item)
                 (str result "<li>" value (render-items (:items inner-item)) "</li>")
                 (str result "<li>" value "</li>"))
               (drop 1 inner-items))))))


(defn render
  "Renders the list block"
  [block _]
  (-> (compose-item-tree block)
      (render-items)))