packages feed

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

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

-- |
-- Module      :  Text.MMark.Extension.LineHighlight
-- Copyright   :  © 2026–present Mark Karpov
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- Point at the lines of a code block that the prose is about.
--
-- Write the lines to point at after the language in the info string:
--
-- > ```haskell {2,4-6}
-- > …
-- > ```
--
-- 'Text.MMark.Extension.Skylighting.skylighting' and
-- 'Text.MMark.Extension.GhcSyntaxHighlighter.ghcSyntaxHighlighter' read the
-- same specification and point at the lines themselves, around the tokens
-- they have coloured. Put either of them before this extension and it takes
-- the blocks whose language it knows; this one renders the rest, without
-- colouring but with the lines still pointed at.
--
-- @since 0.3.0.0
module Text.MMark.Extension.LineHighlight
  ( lineHighlight,
    parseLineSpec,
  )
where

import Data.Text (Text)
import Data.Text qualified as T
import Lucid
import Text.MMark.Extension.Internal (lineSpec, withLineHighlight)
import Text.MMark.Render (Block (..), RenderExtension)
import Text.MMark.Render qualified as Render

-- | Render a code block whose info string ends with a line specification,
-- giving the lines it names the class @\"highlighted-line\"@.
--
-- The language, if there is one, still becomes the @language-@ class of the
-- @\<code\>@ element, so this composes with a style sheet written for the
-- usual output.
lineHighlight :: RenderExtension
lineHighlight = Render.blockRender $ \old block ->
  case block of
    b@(CodeBlock _ (Just info) txt) ->
      case parseLineSpec info of
        Nothing -> old b
        Just (lang, ns) -> do
          pre_
            $ code_ (langAttr lang)
            $ mapM_ (line ns) (zip [1 :: Int ..] (T.lines txt))
          "\n"
    other -> old other
  where
    langAttr = \case
      Just l | not (T.null l) -> [class_ ("language-" <> l)]
      _ -> []
    line ns (n, t) = withLineHighlight ns n (toHtml (t <> "\n"))

-- | Split an info string into the language and the lines to point at.
-- Gives 'Nothing' when there is no line specification, so that an ordinary
-- code block is left to whatever renders it.
--
-- > parseLineSpec "haskell {2,4-6}" == Just (Just "haskell", [2,4,5,6])
parseLineSpec :: Text -> Maybe (Maybe Text, [Int])
parseLineSpec = lineSpec