A zero-dependency, runtime-agnostic HTML parser and builder.
  • Clojure 73.7%
  • HTML 26.3%
Find a file
2026-03-13 11:16:04 +02:00
.clj-kondo Remove manual hooks in favor of a simple lint-as rule 2025-10-13 02:57:33 +03:00
.forgejo/workflows Update Zulu JDK to 24.0.2 (zulu24.32.13) 2026-01-25 15:25:46 +02:00
src/dompa Update docs 2026-02-11 16:48:43 +02:00
test/dompa Allow docstrings in defhtml (closes #2) 2026-02-11 01:53:02 +02:00
.gitignore Do away with the $ macro for runtime-agnostic purposes 2025-10-09 22:05:47 +03:00
API.md Update docs 2026-02-11 16:48:43 +02:00
bb.edn Update docs 2026-02-11 16:48:43 +02:00
deps.edn Add alias for test repl 2025-10-26 11:21:58 +02:00
LICENSE.txt Add barebones README, add license 2025-09-27 20:03:34 +03:00
README.md Update README.md 2026-03-13 11:16:04 +02:00

Dompa

A zero-dependency, runtime-agnostic HTML parser and builder for Clojure.

Dompa aims to be a universal Clojure library, tested across:

  • Clojure
  • ClojureScript
  • Babashka

🚀 Installation

Add Dompa to your deps.edn:

{:deps {askonomm/dompa {:git/url "https://git.nmm.ee/asko/dompa.git"
                        :git/tag "v1.2.3"
                        :git/sha "a8696e088efedf310929bc4e01eded36d41414b1"}}}

Usage

Parsing and Creating HTML

Dompa makes it simple to convert HTML strings into Clojure data structures and back.

1. Parse HTML to Nodes

Use dompa.html/->nodes to parse an HTML string into a vector of nodes.

(ns my.app
  (:require [dompa.html :as html]))

(html/->nodes "<div>hello <strong>world</strong></div>")

This produces a nested data structure:

[{:node/name :div
  :node/attrs {}
  :node/children [{:node/name :dompa/text
                   :node/value "hello "}
                  {:node/name :strong
                   :node/attrs {}
                   :node/children [{:node/name :dompa/text
                                    :node/value "world"}]}]}]

2. Create HTML from Nodes

Use dompa.nodes/->html to convert the node structure back into an HTML string.

(ns my.app
  (:require [dompa.nodes :as nodes]))

;; ...using the nodes from the previous example
(nodes/->html [...])
;;=> "<div>hello <strong>world</strong></div>"

Traversing and Modifying Nodes

Easily walk and transform the node tree with the dompa.nodes/traverse helper.

(ns my.app
  (:require [dompa.nodes :refer [traverse]])

(def nodes-data [...]) ; Your node structure

(defn update-text-value [node]
  (if (= :dompa/text (:node/name node))
    (assoc node :node/value "updated text")
    node))

(traverse nodes-data update-text-value)

The function you provide to traverse dictates the outcome for each node:

  • To update a node, return the modified node.
  • To keep a node unchanged, return the original node.
  • To remove a node, return nil.
  • To replace a node with many nodes on the same level, return a fragment node, which will be replaced by its children during HTML transformation.

Zipping

You can also use the dompa.nodes/zip function to create a zipper of a node, i.e:

(ns my.app
  (:require [dompa.nodes :as nodes]
            [clojure.zip :as zip])

(->> (nodes/zip {..node})
     zip/down
     zip/node)

And of course you can use this in combination with the traverse method as well, since the traverse method always operates on a single node at a time, and the zipper always starts with a root node, the two complement each other well.


🛠️ Building Nodes with the $ Helper

For a more idiomatic and concise way to build node structures, use the $ helper from dompa.nodes.

(ns my.app
  (:require [dompa.nodes :refer [$]]))

;; A simple node
($ :button)

;; A node with attributes
($ :button {:class "some-btn"})

;; A text node
($ "hello world")

;; Put it all together
($ :button {:class "some-btn"}
  "hello world")

All nodes (except text nodes) can be nested. Children are passed as the second argument (if no attributes) or the third argument (if attributes are present).

Fragment nodes

You can also use fragment nodes, which are nodes with a name of :<> and whose children replace themselves, for example:

(ns my.app
  (:require [dompa.nodes :refer [$ ->html]]))

(->html [($ :button
          ($ :<> "hello " "world"))])
;;=> <div>hello world</div>

Note that the replacement happens only in the ->html function during the transformation process, so if you use a fragment node in your node tree and wonder why it hasn't been replaced by its children, it's because of that.


Compile-Time HTML with defhtml

The defhtml macro creates functions that build and render HTML at compile time for maximum performance.

(ns my.app
  (:require [dompa.nodes :refer [defhtml $]]))

(defhtml hello-page [who]
  ($ :div "hello " who))

(hello-page "world")
;;=> "<div>hello world</div>"

It works seamlessly with standard Clojure functions like map:

(ns my.app
  (:require [dompa.nodes :refer [defhtml $]]))

(def names ["john" "mike" "jenna"])

(defhtml name-list []
  ($ :ul
    (map #($ :li %) names)))

(name-list)
;;=> "<ul><li>john</li><li>mike</li><li>jenna</li></ul>"

Docstrings

defhtml supports optional docstrings, just like defn:

(defhtml about-page
  "Renders the about page for a person."
  [who]
  ($ :div "Hello, " who "!"))

Composition

Functions created with defhtml can be nested and composed with each other:

(defhtml greeting [who]
  ($ :span who))

(defhtml page [who]
  ($ :div
     (greeting who)))

(page "world")
;;=> "<div><span>world</span></div>"

Note for ClojureScript: Remember to use :refer-macros instead of :refer when requiring defhtml.


⚙️ Advanced: Lower-Level API

Dompa also exposes the lower-level functions that power the parsing process. You can use these for more granular control:

  • dompa.coordinates/compose: Creates range positions of nodes from an HTML string.
  • dompa.coordinates/unify: Merges coordinates for the same block nodes.
  • dompa.coordinates/->nodes: Transforms coordinate data into a final node tree.
  • dompa.html/->coordinates: Transforms an HTML string into coordinate data (result of dompa.coordinates/compose and dompa.coordinates/unify).