Liking cljdoc? Tell your friends :D

Montalivet

Clojars Project

Clojure HTML toolkit: parse, clean and escape, powered by jsoup.

A small Clojure wrapper around jsoup, so you can parse HTML, escape text and sanitise untrusted markup from Clojure without writing Java interop yourself.

(require '[io.github.papachan.montalivet :as m])

(-> (m/parse "<p>1 < 2</p>" {:pretty-print false :escape-mode :extended})
    m/body-html)
;; => "<p>1 &lt; 2</p>"

(m/escape-html "<b>a & b</b>")
;; => "&lt;b&gt;a &amp; b&lt;/b&gt;"

(m/clean "<p onclick=\"x()\">Hi <script>alert(1)</script></p>")
;; => "<p>Hi </p>"

Installation

Add it to your deps.edn:

io.github.papachan/montalivet {:mvn/version "0.1.1"}

Install it locally from a clone:

clojure -T:build install

Usage

Everything public is in io.github.papachan.montalivet.

parse

(m/parse html)
(m/parse html opts)

Parses an HTML string and returns a jsoup Document.

OptionDefaultDescription
:pretty-printtrueReformat the output when the document is rendered.
:escape-modejsoup's default:base, :xhtml or :extended.

An unknown :escape-mode throws an ex-info listing the valid modes.

body-html

(m/body-html doc)

Returns the inner HTML of the document's <body> as a string. Fragments without a <body> are wrapped in one by jsoup, so this works on partial markup too:

(-> (m/parse "<b>a</b><i>b</i>" {:pretty-print false})
    m/body-html)
;; => "<b>a</b><i>b</i>"

escape-html

(m/escape-html s)
(m/escape-html s opts)

Escapes a string so it can be embedded as HTML text. It takes the same :escape-mode option as parse (default :base).

(m/escape-html "a b" {:escape-mode :extended})
;; => "a&nbsp;b"

(m/escape-html "a b" {:escape-mode :xhtml})
;; => "a&#xa0;b"   ; XHTML has no &nbsp;

Jsoup only escapes characters the output charset cannot represent. With the default UTF-8, text such as café is returned unchanged, so this is not identical to Apache Commons Text's escapeHtml4.

clean

(m/clean html)
(m/clean html opts)

Removes everything that is not allowed by a safelist, so untrusted markup can be embedded safely. Returns a string.

(m/clean "<p onclick=\"x()\">Hi <script>alert(1)</script><b>there</b></p>")
;; => "<p>Hi <b>there</b></p>"

;; keep only the text
(m/clean "<p>Hi <b>there</b></p>" {:safelist :none})
;; => "Hi there"

;; allow extra tags and attributes
(m/clean "<section class=\"c\"><b>x</b></section>"
         {:add-tags [:section] :add-attributes {:section [:class]}})
;; => "<section class=\"c\"><b>x</b></section>"

;; relative links need a base URI, and unsafe ones lose their href
(m/clean "<a href=\"/a\">l</a>" {:base-uri "https://example.com/"})
;; => "<a href=\"https://example.com/a\">l</a>"

(m/clean "<a href=\"javascript:alert(1)\">l</a>" {:base-uri "https://example.com/"})
;; => "<a rel=\"nofollow\">l</a>"
OptionDefaultDescription
:safelist:basic:none, :simple-text, :basic, :basic-with-images or :relaxed.
:add-tags, :remove-tags Tags to allow or disallow, such as [:section].
:add-attributes, :remove-attributes Map of tag to attributes, such as {:a [:target] :all [:class]}. :all applies to every tag.
:base-uri""Base used to resolve relative links. Without it they are removed.
:preserve-relative-linksfalseKeep relative URLs as they are. Still requires :base-uri.
:pretty-printfalseReformat the output.
:escape-mode :base, :xhtml or :extended.

An unknown :safelist or :escape-mode throws an ex-info listing the valid values. The exact output can differ between jsoup versions, so check it against the version you use.

select and select-one

(m/select doc css-query)
(m/select-one doc css-query)

Find elements of a parsed document with a CSS selector. select returns a vector of matching elements in document order (empty when nothing matches), and select-one returns the first match or nil. Both work on a document or on any element, in which case only its descendants are searched. An empty or invalid selector throws an exception.

(def doc (m/parse "<div id=\"a\" class=\"x y\">
                     <a href=\"/1\" title=\"one\">One</a>
                     <a href=\"/2\">Two</a>
                   </div>"))

(map m/text (m/select doc "a"))
;; => ("One" "Two")

(map #(m/attr % :href) (m/select doc "a[href]"))
;; => ("/1" "/2")

(m/select doc "table")
;; => []

(m/select-one doc "table")
;; => nil

Reading elements

FunctionReturns
(m/text el)The element's text, with entities decoded.
(m/attr el k)The value of attribute k (string or keyword), or nil.
(m/attrs el)All attributes as a map of keyword to string.
(m/outer-html el)The element's own markup, including its tag.
(m/attrs (m/select-one doc "div"))
;; => {:id "a", :class "x y"}

(m/outer-html (m/select-one doc "a"))
;; => "<a href=\"/1\" title=\"one\">One</a>"

Documentation

You can find the documentation here: API.

Development

Run the tests with Kaocha:

clojure -M:dev:test:runner

Other tasks:

clojure -T:build jar        # build target/montalivet-<version>.jar and pom.xml
clojure -T:build uberjar    # standalone jar with all dependencies
clojure -T:build install    # install into ~/.m2
clojure -M:clj-kondo --lint src test build.clj dev

See CONTRIBUTING.md and the CHANGELOG.

License

Copyright © 2026 papachan.

Licensed under the Apache License, Version 2.0. See LICENSE.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close