mmark-ext-0.3.0.0: Text/MMark/Extension/Permalinks.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Text.MMark.Extension.Permalinks
-- Copyright : © 2026–present Mark Karpov
-- License : BSD 3 clause
--
-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
-- Stability : experimental
-- Portability : portable
--
-- Give every heading a link to itself, so that a reader can get a URL that
-- points at the section they are looking at.
--
-- @since 0.3.0.0
module Text.MMark.Extension.Permalinks
( permalinks,
permalinksWith,
)
where
import Data.Text (Text)
import Lucid
import Lucid.Base (makeAttribute)
import Text.MMark.Render (Block (..), Ois, RenderExtension, getOis)
import Text.MMark.Render qualified as Render
import Text.URI qualified as URI
-- | Append to every heading a link to the id MMark gives that heading. The
-- link is labelled @\"#\"@ and given the class @\"permalink\"@, so that a
-- style sheet can show it only when the heading is hovered.
permalinks :: RenderExtension
permalinks = permalinksWith (const True) "permalink" Nothing "#"
-- | Like 'permalinks', but you choose which headings get a link, the class
-- it is given, what a screen reader makes of it, and what the reader sees.
--
-- The last of these is @'Html' ()@, so the link can be labelled with an
-- icon rather than a character:
--
-- > permalinksWith (\n -> n >= 2 && n <= 4) "anchor" Nothing linkIcon
--
-- A link nothing is to be said about is hidden from a screen reader, and
-- taken out of the order the keyboard walks: a link that is announced to
-- nobody is of no use to someone who has landed on it. Say what it is
-- instead to keep it in:
--
-- > permalinksWith (const True) "anchor" (Just "Link to this section") "#"
permalinksWith ::
-- | Whether to give a heading of this level (1–6) a link
(Int -> Bool) ->
-- | Class to give the link
Text ->
-- | What a screen reader should say, if anything
Maybe Text ->
-- | What the reader sees
Html () ->
RenderExtension
permalinksWith p klass spoken shown = Render.blockRender $ \old block ->
case block of
Heading1 spn x | p 1 -> old (Heading1 spn (anchor x))
Heading2 spn x | p 2 -> old (Heading2 spn (anchor x))
Heading3 spn x | p 3 -> old (Heading3 spn (anchor x))
Heading4 spn x | p 4 -> old (Heading4 spn (anchor x))
Heading5 spn x | p 5 -> old (Heading5 spn (anchor x))
Heading6 spn x | p 6 -> old (Heading6 spn (anchor x))
other -> old other
where
anchor (ois, html) = (ois, html <> link ois)
link :: Ois -> Html ()
link ois =
a_
( href_ (URI.render (Render.headerFragment (Render.headerId (getOis ois))))
: class_ klass
: how
)
shown
how = case spoken of
Just t -> [makeAttribute "aria-label" t]
Nothing ->
[ makeAttribute "aria-hidden" "true",
makeAttribute "tabindex" "-1"
]