{"id":15632490,"url":"https://github.com/borkdude/edamame","last_synced_at":"2025-04-13T22:29:37.712Z","repository":{"id":35146955,"uuid":"204736531","full_name":"borkdude/edamame","owner":"borkdude","description":"Configurable EDN/Clojure parser with location metadata","archived":false,"fork":false,"pushed_at":"2025-03-31T14:03:17.000Z","size":421,"stargazers_count":176,"open_issues_count":1,"forks_count":14,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-06T19:08:57.377Z","etag":null,"topics":["clojure","clojurescript","edn"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/borkdude.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"borkdude"}},"created_at":"2019-08-27T15:45:33.000Z","updated_at":"2025-03-31T14:02:43.000Z","dependencies_parsed_at":"2024-01-30T01:59:34.656Z","dependency_job_id":"616c84ac-bbf9-4291-a9be-a8dfadf8af00","html_url":"https://github.com/borkdude/edamame","commit_stats":{"total_commits":240,"total_committers":11,"mean_commits":"21.818181818181817","dds":"0.050000000000000044","last_synced_commit":"e44318ce4ac4df9c2542b8c709e67eac7d80d1c8"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fedamame","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fedamame/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fedamame/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borkdude%2Fedamame/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/borkdude","download_url":"https://codeload.github.com/borkdude/edamame/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248362014,"owners_count":21091033,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojurescript","edn"],"created_at":"2024-10-03T10:44:24.359Z","updated_at":"2025-04-13T22:29:37.660Z","avatar_url":"https://github.com/borkdude.png","language":"Clojure","funding_links":["https://github.com/sponsors/borkdude"],"categories":[],"sub_categories":[],"readme":"# Edamame\n\nConfigurable EDN/Clojure parser with location metadata.\n\n[![CircleCI](https://circleci.com/gh/borkdude/edamame/tree/master.svg?style=shield)](https://circleci.com/gh/borkdude/edamame/tree/master)\n[![Clojars Project](https://img.shields.io/clojars/v/borkdude/edamame.svg)](https://clojars.org/borkdude/edamame)\n\u003c!--[![cljdoc badge](https://cljdoc.org/badge/borkdude/edamame)](https://cljdoc.org/d/borkdude/edamame/CURRENT)--\u003e\n\n## Reasons to use edamame\n\n- You want to include locations in feedback about Clojure and EDN files\n- You want to parse Clojure-like expressions without any evaluation\n- Anonymous functions are read deterministically\n- Highly configurable\n- Auto-resolve aliased keywords based on the `ns` form\n\nThis library works with:\n\n- Clojure on the JVM\n- GraalVM compiled binaries\n- ClojureScript (including self-hosted and advanced compiled)\n\n## Installation\n\nUse as a dependency:\n\n[![Clojars Project](https://img.shields.io/clojars/v/borkdude/edamame.svg)](https://clojars.org/borkdude/edamame)\n\n\n## Projects\n\nProject using edamame:\n\n- [clerk](https://github.com/nextjournal/clerk)\n- [malli](https://github.com/metosin/malli)\n- [poly](https://cljdoc.org/d/polylith/clj-poly/0.2.18/doc/readme)\n- [SCI](https://github.com/borkdude/sci)\n- [splint](https://github.com/noahtheduke/splint)\n- [zprint](https://github.com/kkinnear/zprint)\n- [zen](https://github.com/zen-lang/zen)\n\n## API\n\nSee [API](API.md).\n\n## Usage\n\n``` clojure\n(require '[edamame.core :as e :refer [parse-string]])\n```\n\n### Location metadata\n\nLocations are attached as metadata:\n\n``` clojure\n(def s \"\n[{:a 1}\n {:b 2}]\")\n(map meta (parse-string s))\n;;=\u003e\n({:row 2, :col 2, :end-row 2, :end-col 8}\n {:row 3, :col 2, :end-row 3, :end-col 8})\n\n(-\u003e\u003e \"{:a {:b {:c [a b c]}}}\"\n     parse-string\n     (tree-seq coll? #(if (map? %) (vals %) %))\n     (map meta))\n;;=\u003e\n({:row 1, :col 1, :end-row 1, :end-col 23}\n {:row 1, :col 5, :end-row 1, :end-col 22}\n {:row 1, :col 9, :end-row 1, :end-col 21}\n {:row 1, :col 13, :end-row 1, :end-col 20}\n {:row 1, :col 14, :end-row 1, :end-col 15}\n {:row 1, :col 16, :end-row 1, :end-col 17}\n {:row 1, :col 18, :end-row 1, :end-col 19})\n```\n\nYou can control on which elements locations get added using the `:location?`\noption.\n\n### Parser options\n\nEdamame's API consists of two functions: `parse-string` which parses a the first\nform from a string and `parse-string-all` which parses all forms from a\nstring. Both functions take the same options. See the docstring of\n`parse-string` for all the options.\n\nExamples:\n\n``` clojure\n(parse-string \"@foo\" {:deref true})\n;;=\u003e (deref foo)\n\n(parse-string \"'bar\" {:quote true})\n;;=\u003e (quote bar)\n\n(parse-string \"#(* % %1 %2)\" {:fn true})\n;;=\u003e (fn [%1 %2] (* %1 %1 %2))\n\n(parse-string \"#=(+ 1 2 3)\" {:read-eval true})\n;;=\u003e (read-eval (+ 1 2 3))\n\n(parse-string \"#\\\"foo\\\"\" {:regex true})\n;;=\u003e #\"foo\"\n\n(parse-string \"#'foo\" {:var true})\n;;=\u003e (var foo)\n\n(parse-string \"#(alter-var-root #'foo %)\" {:all true})\n;;=\u003e (fn [%1] (alter-var-root (var foo) %1))\n```\n\nNote that standard behavior is overridable with functions:\n\n``` clojure\n(parse-string \"#\\\"foo\\\"\" {:regex #(list 're-pattern %)})\n(re-pattern \"foo\")\n```\n\n### Clojure defaults\n\nThe closest defaults to how Clojure reads code:\n\n``` clojure\n{:all true\n :row-key :line\n :col-key :column\n :end-location false\n :location? seq?}\n```\n\n### Reader conditionals\n\nProcess reader conditionals:\n\n``` clojure\n(parse-string \"[1 2 #?@(:cljs [3 4])]\" {:features #{:cljs} :read-cond :allow})\n;;=\u003e [1 2 3 4]\n\n(parse-string \"[1 2 #?@(:cljs [3 4])]\" {:features #{:cljs} :read-cond :preserve})\n;;=\u003e [1 2 #?@(:cljs [3 4])]\n\n(let [res (parse-string \"#?@(:bb 1 :clj 2)\" {:read-cond identity})]\n  (prn res) (prn (meta res)))\n;;=\u003e (:bb 1 :clj 2)\n;;=\u003e {:row 1, :col 1, :end-row 1, :end-col 18, :edamame/read-cond-splicing true}\n```\n\n### Auto-resolve\n\nAuto-resolve keywords:\n\n``` clojure\n(parse-string \"[::foo ::str/foo]\" {:auto-resolve '{:current user str clojure.string}})\n;;=\u003e [:user/foo :clojure.string/foo]\n```\n\nIf you don't care much about the exact value of the keyword, but just want to parse something:\n\n``` clojure\n(parse-string \"[::foo ::str/foo]\" {:auto-resolve name})\n;;=\u003e [:current/foo :str/foo]\n```\n\nTo create options from a namespace in the process where edamame is called from:\n\n``` clojure\n(defn auto-resolves [ns]\n  (as-\u003e (ns-aliases ns) $\n    (assoc $ :current (ns-name *ns*))\n    (zipmap (keys $)\n            (map ns-name (vals $)))))\n\n(require '[clojure.string :as str]) ;; create example alias\n\n(auto-resolves *ns*) ;;=\u003e {str clojure.string, :current user}\n\n(parse-string \"[::foo ::str/foo]\" {:auto-resolve (auto-resolves *ns*)})\n;;=\u003e [:user/foo :clojure.string/foo]\n```\n\nTo auto-resolve keywords from the running Clojure environment:\n\n``` clojure\n(require '[clojure.test :as t])\n(e/parse-string \"::t/foo\" {:auto-resolve (fn [x] (if (= :current x) *ns* (get (ns-aliases *ns*) x)))})\n:clojure.test/foo\n```\n\n#### `:auto-resolve-ns`\n\nTo auto-magically resolve keywords based on the `ns` form, use `:auto-resolve-ns\ntrue`:\n\n``` clojure\n(= '[(ns foo (:require [clojure.set :as set])) :clojure.set/foo]\n    (parse-string-all \"(ns foo (:require [clojure.set :as set])) ::set/foo\"\n                      {:auto-resolve-ns true}))\n\n(def rdr (p/reader \"(ns foo (:require [clojure.set :as set])) ::set/foo\"))\n(def opts (p/normalize-opts {:auto-resolve-ns true}))\n(= (ns foo (:require [clojure.set :as set])) (p/parse-next rdr opts))\n(= :clojure.set/foo (p/parse-next rdr opts))\n```\n\n### Syntax-quote\n\nSyntax quoting can be enabled using the `:syntax-quote` option. Symbols are\nresolved to fully qualified symbols using `:resolve-symbol` which is set to\n`identity` by default:\n\n``` clojure\n(parse-string \"`(+ 1 2 3 ~x ~@y)\" {:syntax-quote true})\n;;=\u003e (clojure.core/sequence (clojure.core/seq (clojure.core/concat (clojure.core/list (quote +)) (clojure.core/list 1) (clojure.core/list 2) (clojure.core/list 3) (clojure.core/list x) y)))\n\n(parse-string \"`(+ 1 2 3 ~x ~@y)\" {:syntax-quote {:resolve-symbol #(symbol \"user\" (name %))}})\n;;=\u003e (clojure.core/sequence (clojure.core/seq (clojure.core/concat (clojure.core/list (quote user/+)) (clojure.core/list 1) (clojure.core/list 2) (clojure.core/list 3) (clojure.core/list x) y)))\n```\n\nTo resolve symbols in syntax quote from the running Clojure environment:\n\n``` clojure\n(require '[clojure.tools.reader :refer [resolve-symbol]])\n\n(require '[clojure.test :as t])\n(e/parse-string \"`t/run-tests\" {:syntax-quote {:resolve-symbol resolve-symbol}})\n;;=\u003e (quote clojure.test/run-tests)\n```\n\n### Data readers\n\nPassing data readers:\n\n``` clojure\n(parse-string \"#js [1 2 3]\" {:readers {'js (fn [v] (list 'js v))}})\n(js [1 2 3])\n```\n\n### Preserve order of map and set keys\n\nTo preserve order of map and set keys, you can use the `:map` and `:set` options:\n\n``` clojure\n(require '[edamame.core :as e])\n(require '[flatland.ordered.map :as m])\n(e/parse-string \"{:a 1}\" {:map m/ordered-map}) ;;=\u003e #ordered/map ([:a 1])\n(require '[clojure.data.json :as j])\n(j/write-str (e/parse-string \"{:a 1 :b 2 :c 3 :d 4 :e 5 :f 6 :g 7 :h 8 :i 9 :j 10 :k 11 :l 12}\" {:map m/ordered-map}))\n;;=\u003e \"{\\\"a\\\":1,\\\"b\\\":2,\\\"c\\\":3,\\\"d\\\":4,\\\"e\\\":5,\\\"f\\\":6,\\\"g\\\":7,\\\"h\\\":8,\\\"i\\\":9,\\\"j\\\":10,\\\"k\\\":11,\\\"l\\\":12}\"\n```\n\n### Postprocess\n\nPostprocess read values:\n\n``` clojure\n(defrecord Wrapper [obj loc])\n\n(defn iobj? [x]\n  #?(:clj (instance? clojure.lang.IObj x)\n     :cljs (satisfies? IWithMeta x)))\n\n(parse-string \"[1]\" {:postprocess\n                       (fn [{:keys [:obj :loc]}]\n                         (if (iobj? obj)\n                           (vary-meta obj merge loc)\n                           (-\u003eWrapper obj loc)))})\n\n[#user.Wrapper{:obj 1, :loc {:row 1, :col 2, :end-row 1, :end-col 3}}]\n```\n\nThis allows you to preserve metadata for objects that do not support carrying\nmetadata. When you use a `:postprocess` function, it is your responsibility to\nattach location metadata.\n\n### Fix incomplete expressions\n\nEdamame exposes information via `ex-data` in an exception in case of unmatched\ndelimiters. This can be used to fix incomplete expressions:\n\n``` clojure\n(def incomplete \"{:a (let [x 5\")\n\n(defn fix-expression [expr]\n  (try (when (parse-string expr)\n         expr)\n       (catch clojure.lang.ExceptionInfo e\n         (if-let [expected-delimiter (:edamame/expected-delimiter (ex-data e))]\n           (fix-expression (str expr expected-delimiter))\n           (throw e)))))\n\n(fix-expression incomplete) ;; =\u003e \"{:a (let [x 5])}\"\n```\n\n## Test\n\nFor the node tests, ensure clojure is installed as a command line tool as shown [here](https://clojure.org/guides/getting_started#_installation_on_mac_via_homebrew). For the JVM tests you will require [leiningen](https://leiningen.org/) to be installed. Then run the following:\n\n    script/test/jvm\n    script/test/node\n    script/test/all\n\n## Credits\n\nThe code is largely inspired by\n[rewrite-clj](https://github.com/xsc/rewrite-clj) and derived projects.\n\n## License\n\nCopyright © 2019-2022 Michiel Borkent\n\nDistributed under the Eclipse Public License 1.0. This project contains code\nfrom Clojure and ClojureScript which are also licensed under the EPL 1.0. See\nLICENSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborkdude%2Fedamame","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fborkdude%2Fedamame","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborkdude%2Fedamame/lists"}