init
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
(ns tui.render
|
||||
"Render hiccup to ANSI strings."
|
||||
(:require [tui.ansi :as ansi]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; === Hiccup Parsing ===
|
||||
(defn- parse-element
|
||||
"Parse hiccup element into [tag attrs children]."
|
||||
[elem]
|
||||
(cond
|
||||
(string? elem) [:text {} [elem]]
|
||||
(number? elem) [:text {} [(str elem)]]
|
||||
(nil? elem) [:text {} [""]]
|
||||
(vector? elem)
|
||||
(let [[tag & rest] elem
|
||||
[attrs children] (if (map? (first rest))
|
||||
[(first rest) (vec (next rest))]
|
||||
[{} (vec rest)])]
|
||||
[tag attrs children])
|
||||
:else [:text {} [(str elem)]]))
|
||||
|
||||
;; === Text Rendering ===
|
||||
(defn- apply-style
|
||||
"Apply style attributes to text."
|
||||
[text {:keys [fg bg bold dim italic underline inverse strike]}]
|
||||
(if (or fg bg bold dim italic underline inverse strike)
|
||||
(ansi/style text
|
||||
:fg fg :bg bg
|
||||
:bold bold :dim dim :italic italic
|
||||
:underline underline :inverse inverse :strike strike)
|
||||
text))
|
||||
|
||||
(defn- render-text
|
||||
"Render :text element."
|
||||
[attrs children]
|
||||
(let [content (apply str (flatten children))]
|
||||
(apply-style content attrs)))
|
||||
|
||||
;; === Layout Primitives ===
|
||||
(declare render-element)
|
||||
|
||||
(defn- render-children
|
||||
"Render all children and return list of rendered strings."
|
||||
[children ctx]
|
||||
(mapv #(render-element % ctx) children))
|
||||
|
||||
(defn- render-row
|
||||
"Render :row - horizontal layout."
|
||||
[{:keys [gap justify align] :or {gap 0}} children ctx]
|
||||
(let [rendered (render-children children ctx)
|
||||
separator (apply str (repeat gap " "))]
|
||||
(str/join separator rendered)))
|
||||
|
||||
(defn- render-col
|
||||
"Render :col - vertical layout."
|
||||
[{:keys [gap] :or {gap 0}} children ctx]
|
||||
(let [rendered (render-children children ctx)
|
||||
separator (str/join (repeat gap "\n"))]
|
||||
(str/join (str "\n" separator) rendered)))
|
||||
|
||||
(defn- render-box
|
||||
"Render :box - bordered container."
|
||||
[{:keys [border title padding width]
|
||||
:or {border :rounded padding 0}}
|
||||
children ctx]
|
||||
(let [chars (get ansi/box-chars border (:rounded ansi/box-chars))
|
||||
content (str/join "\n" (render-children children ctx))
|
||||
lines (str/split content #"\n" -1)
|
||||
|
||||
;; Calculate padding
|
||||
[pad-top pad-right pad-bottom pad-left]
|
||||
(cond
|
||||
(number? padding) [padding padding padding padding]
|
||||
(vector? padding)
|
||||
(case (count padding)
|
||||
1 (let [p (first padding)] [p p p p])
|
||||
2 (let [[v h] padding] [v h v h])
|
||||
4 padding
|
||||
[0 0 0 0])
|
||||
:else [0 0 0 0])
|
||||
|
||||
;; Calculate content width
|
||||
max-content-width (apply max 0 (map ansi/visible-length lines))
|
||||
inner-width (+ max-content-width pad-left pad-right)
|
||||
box-width (or width (+ inner-width 2))
|
||||
content-width (- box-width 2)
|
||||
|
||||
;; Pad lines
|
||||
padded-lines (for [line lines]
|
||||
(str (apply str (repeat pad-left " "))
|
||||
(ansi/pad-right line (- content-width pad-left pad-right))
|
||||
(apply str (repeat pad-right " "))))
|
||||
|
||||
;; Add vertical padding
|
||||
empty-line (apply str (repeat content-width " "))
|
||||
all-lines (concat
|
||||
(repeat pad-top empty-line)
|
||||
padded-lines
|
||||
(repeat pad-bottom empty-line))
|
||||
|
||||
;; Build box
|
||||
top-line (str (:tl chars)
|
||||
(if title
|
||||
(str " " title " "
|
||||
(apply str (repeat (- content-width (count title) 3) (:h chars))))
|
||||
(apply str (repeat content-width (:h chars))))
|
||||
(:tr chars))
|
||||
bottom-line (str (:bl chars)
|
||||
(apply str (repeat content-width (:h chars)))
|
||||
(:br chars))
|
||||
body-lines (for [line all-lines]
|
||||
(str (:v chars)
|
||||
(ansi/pad-right line content-width)
|
||||
(:v chars)))]
|
||||
(str/join "\n" (concat [top-line] body-lines [bottom-line]))))
|
||||
|
||||
(defn- render-space
|
||||
"Render :space - empty space."
|
||||
[{:keys [width height] :or {width 1 height 1}} _ _]
|
||||
(let [line (apply str (repeat width " "))]
|
||||
(str/join "\n" (repeat height line))))
|
||||
|
||||
;; === Main Render Function ===
|
||||
(defn render-element
|
||||
"Render a hiccup element to ANSI string."
|
||||
[elem ctx]
|
||||
(cond
|
||||
;; Raw string - just return it
|
||||
(string? elem) elem
|
||||
|
||||
;; Number - convert to string
|
||||
(number? elem) (str elem)
|
||||
|
||||
;; Nil - empty string
|
||||
(nil? elem) ""
|
||||
|
||||
;; Vector - hiccup element
|
||||
(vector? elem)
|
||||
(let [[tag attrs children] (parse-element elem)]
|
||||
(case tag
|
||||
:text (render-text attrs children)
|
||||
:row (render-row attrs children ctx)
|
||||
:col (render-col attrs children ctx)
|
||||
:box (render-box attrs children ctx)
|
||||
:space (render-space attrs children ctx)
|
||||
;; Default: just render children
|
||||
(apply str (render-children children ctx))))
|
||||
|
||||
;; Anything else - convert to string
|
||||
:else (str elem)))
|
||||
|
||||
(defn render
|
||||
"Render hiccup to ANSI string."
|
||||
([hiccup] (render hiccup {}))
|
||||
([hiccup ctx]
|
||||
(render-element hiccup ctx)))
|
||||
|
||||
;; === Convenience Components ===
|
||||
(defn text
|
||||
"Create a text element."
|
||||
[& args]
|
||||
(if (map? (first args))
|
||||
(into [:text (first args)] (rest args))
|
||||
(into [:text {}] args)))
|
||||
|
||||
(defn row
|
||||
"Create a row (horizontal) layout."
|
||||
[& args]
|
||||
(if (map? (first args))
|
||||
(into [:row (first args)] (rest args))
|
||||
(into [:row {}] args)))
|
||||
|
||||
(defn col
|
||||
"Create a col (vertical) layout."
|
||||
[& args]
|
||||
(if (map? (first args))
|
||||
(into [:col (first args)] (rest args))
|
||||
(into [:col {}] args)))
|
||||
|
||||
(defn box
|
||||
"Create a bordered box."
|
||||
[& args]
|
||||
(if (map? (first args))
|
||||
(into [:box (first args)] (rest args))
|
||||
(into [:box {}] args)))
|
||||
Reference in New Issue
Block a user