Skip to content

Instantly share code, notes, and snippets.

@dvingo
Created September 5, 2024 13:36
Show Gist options
  • Save dvingo/4d9d8ab46985055b6ce07b7a36e9a951 to your computer and use it in GitHub Desktop.
Save dvingo/4d9d8ab46985055b6ce07b7a36e9a951 to your computer and use it in GitHub Desktop.
Recursive clojure reader literal for JS objects and arrays with a variant for css key conversion.
(ns levier.js-reader
#?(:cljs (:require-macros [levier.js-reader]))
(:require [cljs.tagged-literals :as cljs-tl]
[clojure.string :as str])
#?(:clj (:import [cljs.tagged_literals JSValue])))
(defn map-keys [m f] (into (empty m) (map (juxt (comp f key) val) m)))
(defn map-vals [m f] (into (empty m) (map (juxt key (comp f val)) m)))
(defn map->js* [m]
(let [str-part
(str/join ","
(map
(fn [[k]]
(str (if (or (symbol? k) (list? k)) "[~{}]" "~{}")
":~{}"))
m))]
(vary-meta
`(~'js* ~(str "({" str-part "})") ~@(reduce into [] m))
assoc :tag 'object)))
(defn kebab->camel
"Takes a string in kebab-case and converts it to kebabCase.
if the string starts with a '.' then it is returned unchanged."
[prop]
(if (str/starts-with? prop ".")
prop
(if (str/includes? prop "-")
(let [words (->> prop (re-seq #"[a-zA-Z]+") (mapv str/capitalize))]
(-> words
(update 0 str/lower-case)
str/join))
prop)))
(defn -convert-js
"Reader literal to convert a tree of maps and vectors into JavaScript object and arrays.
Keywords are converted to strings, fully qualified keywords are represented the same as a string without the leading ':'.
Unquoted symbols in key position are inserted as dynamic JavaScript key values {[symbol-name]: value}"
[form camelize?]
(cond
(and (map? form) (not (:clj (meta form))))
(let [v
(-> form
(map-keys (fn [k]
;(println "map key: " (pr-str k) (type k))
(cond
(qualified-keyword? k) (str (namespace k) "/" (name k))
(keyword? k) (-> k name (cond-> camelize? kebab->camel))
(number? k) (str k)
:else k)))
(map-vals
(fn [k]
(cond
(qualified-keyword? k)
(str (namespace k) "/" (name k))
(keyword? k)
(name k)
(map? k)
(-convert-js k camelize?)
(vector? k)
(-convert-js k camelize?)
:else k))))]
(map->js* v))
(qualified-keyword? form)
(str (namespace form) "/" (name form))
(keyword? form)
(name form)
(map-entry? form)
form
(list? form)
form
(vector? form)
(cljs-tl/JSValue. (mapv #(-convert-js % camelize?) form))
:else
form))
(defn read-js
"Tagged literal for deeply converting map syntax to javascript objects at compile time."
[form]
(let [out (-convert-js form false)]
;(println "OUTPUT: " (with-out-str (pprint/pprint out)))
out))
(defn read-css
"Tagged literal for deeply converting map syntax to javascript objects intended for css - at compile time.
Any keywords in key position of any maps will be converted to strings and if they are in kebab-case will be converted to camelCase."
[form]
(-convert-js form true)
)
;; Example usage, create data_readers.cljc with the contents:
;; {->js levier.js-reader/read-js
;; css levier.js-reader/read-css}
;; usage #->js {} and #css
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment