packages feed

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

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

-- |
-- Module      :  Text.MMark.Extension.Internal
-- Copyright   :  © 2026–present Mark Karpov
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- Helpers shared by the extensions of this package.
module Text.MMark.Extension.Internal
  ( inlinesOf,
    lineSpec,
    infoStringParts,
    withLineHighlight,
  )
where

import Data.Char (isDigit)
import Data.List.NonEmpty qualified as NE
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Lucid
import Text.MMark.Trans (Bni, Inline (..))

-- | Every inline of a block, including the ones nested inside other
-- inlines and inside the blocks the block contains.
inlinesOf :: Bni -> [Inline]
inlinesOf = foldMap (concatMap go . NE.toList)
  where
    go i =
      i : case i of
        Emphasis _ xs -> nested xs
        Strong _ xs -> nested xs
        Strikeout _ xs -> nested xs
        Subscript _ xs -> nested xs
        Superscript _ xs -> nested xs
        Link _ xs _ _ -> nested xs
        Image _ xs _ _ -> nested xs
        _ -> []
    nested = concatMap go . NE.toList

-- | Split the info string of a code block into the language it names and
-- the lines it points at, as in @haskell {2,4-6}@.
--
-- Gives 'Nothing' when there is no line specification, so that a code block
-- written the usual way is left to whatever renders it.
lineSpec :: Text -> Maybe (Maybe Text, [Int])
lineSpec info = do
  let (before, rest) = T.breakOn "{" info
  spec <- T.stripSuffix "}" =<< T.stripPrefix "{" rest
  ns <- traverse range (T.splitOn "," (T.filter (/= ' ') spec))
  return (language before, concat ns)
  where
    range t = case T.splitOn "-" t of
      [a] -> (: []) <$> number a
      [a, b] -> do
        x <- number a
        y <- number b
        if x <= y then Just [x .. y] else Nothing
      _ -> Nothing
    number t =
      if not (T.null t) && T.all isDigit t
        then Just (read (T.unpack t))
        else Nothing

-- | Like 'lineSpec', but for an info string that need not carry a line
-- specification at all: one that does not simply points at no lines.
--
-- Every extension that renders a code block goes through this, so that a
-- language followed by a line specification is still recognized as that
-- language. Without it @haskell {2}@ looks like the name of a language
-- nobody has, and the block loses its syntax highlighting.
infoStringParts :: Text -> (Maybe Text, [Int])
infoStringParts info = fromMaybe (language info, []) (lineSpec info)

-- | Wrap the rendering of one line of a code block when the line is among
-- the ones pointed at.
withLineHighlight ::
  -- | The lines pointed at
  [Int] ->
  -- | The line being rendered, counting from one
  Int ->
  Html () ->
  Html ()
withLineHighlight ns n
  | n `elem` ns = span_ [class_ "highlighted-line"]
  | otherwise = id

-- | The language an info string names, if it names one.
language :: Text -> Maybe Text
language t =
  let l = T.strip t
   in if T.null l then Nothing else Just l