(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)))