janus/text

Convert documents and HTML fragments into text that callers can display, search, or send to a language model.

Require janus.text.api. Do not require the internal namespaces. The HTML map shape is janus.entities.specs.html.

Vocabulary and internals live next to the code in components/text/doc/. This page is a how-to.

1. Add the dependency and require the API

;; deps.edn
{janus/text {:local/root "components/text"}}
(require '[janus.text.api :as text])

2. Parse an HTML string

Use this when Livingdocs or another source gives you a string.

(text/parse-html-fragment "Hello <em>world</em>")
;; => ["Hello " {:tag :em :attrs nil :content ["world"]}]

The result is a Janus HTML fragment: a vector of strings and element maps. An element has :tag, optional :attrs, and optional :content.

Strip tags when you only need the words:

(text/->text (text/parse-html-fragment "Hey <em>Joe</em>"))
;; => "Hey Joe"

Walk every node with the Rama path text/HTML-NODES.

3. Turn Janus HTML into markdown

(text/html->markdown
  [{:tag :p :content ["Hello " {:tag :em :content ["world"]}]}
   {:tag :h2 :content ["Next"]}])
;; => "Hello *world*\n\n## Next"

html→markdown accepts a fragment, an element, a string, or nil. nil becomes "". The function trims the string. It collapses three or more newlines to two.

Unknown tags unwrap. Their children stay. The tag disappears.

(text/html->markdown {:tag :div :content [{:tag :p :content ["Hi"]}]})
;; => "Hi"

4. Stream markdown onto a Writer

Use html→markdown-to! when you already have a destination, for example an HTTP response writer.

(import '[java.io StringWriter])

(let [out (StringWriter.)]
  (text/html->markdown-to! out [{:tag :p :content ["Hello"]}])
  (str out))

The function writes onto any java.lang.Appendable. It returns that same object. It flushes a Writer. It does not close it. It does not trim or collapse whitespace.

Wrap an OutputStream first:

(text/html->markdown-to! (clojure.java.io/writer response-stream) html)

5. Convert an entity tree to HTML, then to markdown

An entity is a document tree. Leaves hold Janus HTML under keys in the janus.html namespace.

(def para
  {:janus.node/tag :paragraph
   :content        {:janus.html/text
                    (text/parse-html-fragment "Hello <em>world</em>")}})

(text/entity->html para)
;; => ["Hello " {:tag :em :attrs nil :content ["world"]}]

(text/html->markdown (text/entity->html para))
;; => "Hello *world*"

entity→html takes keyword args. Pass :variant that way. html→markdown takes an opts map.

(-> entity
    (text/entity->html :variant :sve)
    (text/html->markdown))

The default entity conversion concatenates :janus.html/* on a leaf. It wraps each child segment of a non-leaf in a :section. That is enough for simple paragraphs. It is not enough for images, headers, or lists. Those need a method. See [extend-entity].

A figure URL that lives on the entity is not in the HTML tree. Markdown will not show it until a method emits an img or figure.

6. Convert a whole article for a language model

Require the tenant methods before you convert. Minerva does that with janus.sve.minerva.text.

(require '[janus.sve.minerva.text])   ; registers Livingdocs methods
(require '[janus.text.api :as text])

(defn article->markdown [article]
  (text/html->markdown
    (text/entity->html article :variant :sve)))

Load the method namespace for its side effects. Call entity→html only after that require.

7. Extend entity conversion

entity→html dispatches on the node tag through text/node→html-method. A method receives the node and opts. It must return a Janus HTML fragment.

(defmethod text/node->html-method :paragraph
  [node _opts]
  (if-let [content (not-empty (get-in node [:content :janus.html/text]))]
    [{:tag :p :content content}]
    []))

Two Livingdocs names for the same component share one method. Derive the extra name:

(text/derive-node-tag :p :paragraph)

A tenant-specific method dispatches on [variant node-tag]. Pass the variant into entity→html.

(defmethod text/node->html-method [:sve :subtitle]
  [node _opts]
  [{:tag :h2 :content (get-in node [:content :janus.html/subtitle])}])

(text/entity->html subtitle-node :variant :sve)

Without :variant, a [variant tag] method is not used. A missing pair falls back to the method for the tag.

8. Extend markdown conversion

html→markdown dispatches on the HTML :tag through text/write-html-method. A method receives [rf opts out el] and must return out. rf writes children. out is the Appendable.

(defmethod text/write-html-method :figure
  [rf opts out el]
  (reduce rf out (or (:content el) [])))

Unknown tags already unwrap. Add a method only when the default is wrong.

Built-in tags include headings, p, emphasis, links, lists, img, code, pre, blockquote, br, and hr.

9. Choose the right function

Need Call

HTML string from Livingdocs

parse-html-fragment

Words only

→text

Markdown string for a prompt

html→markdown

Markdown on an HTTP stream

html→markdown-to!

Entity document to HTML

entity→html

Tenant-specific entity HTML

(entity→html node :variant :sve) then require the tenant ns

Internals

Architecture, vocabulary, and the decision records live in components/text/doc/.