packages feed

mmark-ext-0.3.0.0: Text/MMark/Extension/Mermaid.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      :  Text.MMark.Extension.Mermaid
-- Copyright   :  © 2026–present Mark Karpov
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- Turn code blocks with the @mermaid@ info string into diagrams, either in
-- the browser or ahead of time.
--
-- @since 0.3.0.0
module Text.MMark.Extension.Mermaid
  ( -- * In the browser
    mermaid,

    -- * Ahead of time
    mermaidScanner,
    mermaidSvg,
  )
where

import Control.Foldl qualified as L
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Text (Text)
import Lucid
import Text.MMark qualified as MMark
import Text.MMark.Render (Block (..), Bni, RenderExtension, Span)
import Text.MMark.Render qualified as Render

-- | Render a @mermaid@ code block as @\<pre class=\"mermaid\"\>@, which is
-- what the mermaid script in the page looks for.
mermaid :: RenderExtension
mermaid = Render.blockRender $ \old block ->
  case block of
    b@(CodeBlock _ mlabel txt) ->
      if mlabel == Just label
        then pre_ [class_ label] (toHtml txt) >> "\n"
        else old b
    other -> old other

-- | Collect the source of every @mermaid@ code block, by the span of the
-- block it came from.
--
-- Hand the result to whatever turns a diagram into an SVG, then give the
-- SVGs to 'mermaidSvg':
--
-- > srcs <- pure (MMark.runScanner mermaidScanner doc)
-- > svgs <- traverse mermaidCli srcs
-- > TL.putStr (renderText (MMark.render (mermaidSvg svgs) doc))
--
-- The span is the key because it is what tells two diagrams apart, even
-- two that contain exactly the same source.
mermaidScanner :: L.Fold Bni (Map Span Text)
mermaidScanner = MMark.scanner M.empty $ \acc block ->
  case block of
    CodeBlock spn (Just l) txt | l == label -> M.insert spn txt acc
    _ -> acc

-- | Put the given SVG in place of the @mermaid@ code block it was made
-- from. A block with no SVG is left as it is, so that a diagram that could
-- not be rendered is still visible as its source.
mermaidSvg :: Map Span Text -> RenderExtension
mermaidSvg svgs = Render.blockRender $ \old block ->
  case block of
    b@(CodeBlock spn (Just l) _)
      | l == label ->
          case M.lookup spn svgs of
            Just svg -> figure_ [class_ label] (toHtmlRaw svg) >> "\n"
            Nothing -> old b
    other -> old other

label :: Text
label = "mermaid"