Created
September 5, 2024 13:36
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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