nextjournal 2025-09-04

hi! i'm trying to render markdown footnotes to HTML. i see that md/parse preserves them (out-of-line, not in :content), but when i call md/->hiccup, they get discarded:

user=> note
"abc [^1]\n\n[^1]: def"
user=> (md/parse note)
{:toc {:type :toc}, :footnotes [{:type :footnote, :label "1", :content [{:type :paragraph, :content [{:type :text, :text "def"}]}], :ref 0}], :content [{:type :paragraph, :content [{:type :text, :text "abc "} {:type :footnote-ref, :ref 0, :label "1"}]}], :type :doc}
user=> (md/->hiccup note)
[:div [:p "abc " [:sup.sidenote-ref {:data-label "1"} "1"]]]
i also see that this happens on the live demo on https://nextjournal.github.io/markdown/notebooks/try/. what is the intended way to use footnotes with nextjournal?

Good question!

this seemed to work ok 🙂

(defn render-md [src]
  (let [parsed (md/parse src)
        footnotes (map md/->hiccup (:footnotes parsed))
        rendered (md/->hiccup parsed)
        combined (concat rendered [[:hr]] footnotes)]
    (h/html combined)))

yeah, I don't think there's a built-in handling of footnotes, we have some transformation to generate sidenotes as this is the usecase we had in Clerk https://github.com/nextjournal/markdown/blob/51d2b58168def75b16a89693b74ee74d6cf3e0a7/test/nextjournal/markdown_test.cljc#L780-L823

what is the difference between a :sidenote and a :footnote? how does a :sidenote get generated?

the second link I pasted above e.g

(-> "Text[^firstnote] and^[inline _note_ here].

Par.

- again[^note2]
- here

[^firstnote]: Explain 1
[^note2]: Explain 2
"
      md/parse
      u/insert-sidenote-containers)

i'm confused - those look like footnotes to me

yeah, the function u/insert-sidenote-containers builds sidenotes, I think it reflows data to be compatible with the sidenote type renderers

:sidenote-container
:sidenote-column
:sidenote-ref
:sidenote 

oh, so you're saying they're parsed as :footnotes, and then insert-sidenote transforms the AST into :sidenotes, before they're transformed to hiccup?

yes, but that's a very specific markup, I think what you came up with above is also nice

you can also move that into the :doc renderer

(nextjournal.markdown/->hiccup
 (assoc nextjournal.markdown/default-hiccup-renderers
        :doc (fn [_ node]
               (concat [(nextjournal.markdown/->hiccup node)]
                       [[:hr]]
                       (map nextjournal.markdown/->hiccup (:footnotes node)))))
 "Text[^firstnote] and^[inline _note_ here].

 Par.

 - again[^note2]
 - here

 [^firstnote]: Explain 1
 [^note2]: Explain 2
 ")

👍 1

and along those lines also customize how the :footnote and :footnote-ref types are rendered

i was just looking at that, yeah

i think the :doc renderer is a little overkill but i do really like the :footnote hook, it's made things a lot easier

what return types does :footnote expect? i gave it a hiccup-style array and it said "Unknown type: ''. in bold.

if there's a way to get md/->hiccup to actually throw an exception instead of returning a markdown string that says "error", that would be very helpful

It seems type is nil?

yes but my question is what format it expects. does it want a single map? how does that account for sibling HTML nodes?

for context, here is :content on a footnote:

[{:type :paragraph, :content [{:type :text, :text "def"}]}]

i can guess what it wants but i worry that it will break the second the markdown is slightly different than what i tested with

there's into-markup, that's what the default renderers are using internally, but i'm not sure if it's intended to be public api <https://github.com/nextjournal/markdown/blob/bdd31ddfb0eda047d009c8ad8da65081093d7714/src/nextjournal/markdown/transform.cljc#L34>

markdown.transform is a public namespace. you can view the API in API.md

👍 1

that said into-hiccup is an alias for it, just with worse docs

skip-wiki basically means: not public

ok. do you have docs anywhere on how to write a custom renderer?

I agree that the docs can be improved. PR welcome

for that function I mean

happy to send a PR once i understand how to do the thing i want to do lol

do you know what hiccup is?

(no offense intended)

yes, i'm familiar

what i don't know is how to recurse on the :content in a footnote. i expected it to be md/->hiccup and that broke

as long as you return valid hiccup, you're good. the functions available are just intended to make it easier, e.g. to render child nodes you want to render according to the defaults

recurse can be done using into-hiccup indeed

e.g. this is how you can render a different "wrapper":

:doc (partial md/into-hiccup [:div.viewer-markdown])

(this is from the README)

this worked

(defn trans-footnote [cx note]
  ; NOTE: we ignore :label for now
   (md/into-hiccup [:li {:id (str "fn-" (:ref note))}] cx note))

ok, i will add some docs to into-hiccup

feedback: no need to insert literal \n in docstrings, just the visible returns are sufficient.

👍 1

merged, thanks!

am i reading correctly that into-hiccup only lets me prepend content, not append? oh wait it's returning hiccup data, i can append to that