nextjournal

jyn 2025-09-04T15:08:09.730039Z

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?

borkdude 2025-09-04T15:09:25.181489Z

Good question!

borkdude 2025-09-04T15:09:42.517279Z

Let's ping @andrea712

jyn 2025-09-04T16:03:02.510599Z

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)))

borkdude 2025-09-04T16:07:35.576609Z

nice!

Andrea 2025-09-04T16:08:24.807789Z

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

jyn 2025-09-04T16:10:15.888269Z

lol

jyn 2025-09-04T16:11:31.097489Z

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

Andrea 2025-09-04T16:12:18.668649Z

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)

jyn 2025-09-04T16:12:45.254569Z

i'm confused - those look like footnotes to me

Andrea 2025-09-04T16:15:31.523349Z

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 

jyn 2025-09-04T16:17:06.094099Z

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?

Andrea 2025-09-04T16:24:56.462969Z

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

Andrea 2025-09-04T16:25:08.079259Z

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
Andrea 2025-09-04T16:26:15.999919Z

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

jyn 2025-09-04T16:30:27.275979Z

i was just looking at that, yeah

jyn 2025-09-04T16:30:44.996539Z

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

jyn 2025-09-04T17:24:09.246929Z

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

jyn 2025-09-04T17:26:39.078019Z

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

borkdude 2025-09-04T17:27:52.891709Z

It seems type is nil?

jyn 2025-09-04T17:28:28.801269Z

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

jyn 2025-09-04T17:29:08.579769Z

for context, here is :content on a footnote:

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

jyn 2025-09-04T17:29:50.644369Z

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

jyn 2025-09-04T17:31:10.847259Z

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>

borkdude 2025-09-04T17:31:38.520949Z

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

👍 1
borkdude 2025-09-04T17:32:05.634389Z

no sorry

jyn 2025-09-04T17:32:18.661529Z

right

jyn 2025-09-04T17:32:39.278219Z

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

borkdude 2025-09-04T17:33:11.756669Z

it used to be public but we decided that those function could go into nextjournal.markdown https://github.com/nextjournal/markdown/blob/16be4ef6b8b5741c5d8f8432dd9ce8b52b1f3f68/src/nextjournal/markdown/transform.cljc#L4

borkdude 2025-09-04T17:33:27.179419Z

skip-wiki basically means: not public

jyn 2025-09-04T17:34:17.251999Z

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

borkdude 2025-09-04T17:34:48.622469Z

I agree that the docs can be improved. PR welcome

borkdude 2025-09-04T17:34:55.370679Z

for that function I mean

jyn 2025-09-04T17:35:03.620649Z

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

borkdude 2025-09-04T17:35:41.081169Z

do you know what hiccup is?

borkdude 2025-09-04T17:35:47.208929Z

(no offense intended)

jyn 2025-09-04T17:35:48.163809Z

yes, i'm familiar

jyn 2025-09-04T17:36:23.565519Z

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

borkdude 2025-09-04T17:36:33.413299Z

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

borkdude 2025-09-04T17:36:52.467739Z

recurse can be done using into-hiccup indeed

borkdude 2025-09-04T17:39:08.949519Z

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

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

borkdude 2025-09-04T17:39:12.317649Z

(this is from the README)

jyn 2025-09-04T17:39:19.107539Z

this worked

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

borkdude 2025-09-04T17:39:35.408629Z

👍

jyn 2025-09-04T17:39:44.382479Z

ok, i will add some docs to into-hiccup

borkdude 2025-09-04T17:39:49.898049Z

❤️

borkdude 2025-09-04T17:50:15.247729Z

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

👍 1
borkdude 2025-09-04T17:54:38.062889Z

merged, thanks!

jyn 2025-09-04T18:10:47.182689Z

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

borkdude 2025-09-04T18:12:03.270149Z

right