packages feed

pandoc-3.12.1: src/Text/Pandoc/Parsing/Math.hs

{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE OverloadedStrings          #-}
{- |
Module      : Text.Pandoc.Parsing.Math
Copyright   : © 2006-2024 John MacFarlane
License     : GPL-2.0-or-later
Maintainer  : John MacFarlane <jgm@berkeley.edu>

Parsing of LaTeX math.
-}

module Text.Pandoc.Parsing.Math
  ( mathDisplay
  , mathInline
  )
where

import Control.Monad (when, guard)
import Data.Text (Text)
import Text.Parsec ((<|>), ParsecT, Stream(..), notFollowedBy, manyTill,
                    many1, try)
import Text.Pandoc.Options
  ( Extension(Ext_tex_math_dollars, Ext_tex_math_single_backslash,
              Ext_tex_math_double_backslash) )
import Text.Pandoc.Parsing.Capabilities (HasReaderOptions, guardEnabled)
import Text.Pandoc.Parsing.General
import Text.Pandoc.Shared (trimMath)
import Text.Pandoc.Sources
  (UpdateSourcePos, anyChar, char, digit, newline, satisfy, space)

import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB

mathWith :: (Stream s m Char, UpdateSourcePos s Char)
               => Text -> Text -> ParsecT s st m Text
mathWith op cl = try $ do
  textStr op
  when (op == "$") $ notFollowedBy space
  words' <- many1Till (
                       mathComment
                   <|> mathGroup
                   <|> (T.singleton <$>
                          satisfy (\c -> not
                                    (isSpaceChar c || c == '\\' || c == '{')))
                   <|> (T.pack <$> many1 spaceChar <*
                          (guard (op /= "$") <|> notFollowedBy (char '$')))
                   <|> ("\n" <$ newline <* notFollowedBy' blankline <*
                          (guard (op /= "$") <|> notFollowedBy (char '$')))
                   <|> (char '\\' >> (\c -> T.pack ['\\',c]) <$> anyChar)
                    ) (try $ textStr cl)
  when (cl == "$") $ notFollowedBy digit  -- to prevent capture of $5
  return $ trimMath $ T.concat words'

-- Preserve comments, but do not interpret their braces or math delimiters.
-- Consume the newline too: it terminates the comment, not the math content.
mathComment :: (Stream s m Char, UpdateSourcePos s Char)
            => ParsecT s st m Text
mathComment = do
  char '%'
  content <- manyTill anyChar newline
  notFollowedBy' blankline
  return $ T.pack ('%' : content ++ "\n")

-- Consume a whole TeX group so delimiters in command arguments (for example,
-- \text{hi $x$ bye} or \colorbox{aqua}{$x$}) cannot close the outer math.
-- Escaped braces do not change the nesting depth.
mathGroup :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Text
mathGroup = do
  char '{'
  TL.toStrict . TB.toLazyText <$> go (1 :: Int) False (TB.singleton '{')
 where
  -- go depth lastWasBackslash accumulator
  go 0 _ acc = return acc
  go depth True acc = do
    c <- mathChar
    go depth False (acc <> TB.singleton c)
  go depth False acc =
    (do comment <- mathComment
        go depth False (acc <> TB.fromText comment))
    <|> do
      c <- mathChar
      let acc' = acc <> TB.singleton c
      case c of
           '\\' -> go depth True acc'
           '}'  -> go (depth - 1) False acc'
           '{'  -> go (depth + 1) False acc'
           _    -> go depth False acc'

mathChar :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Char
mathChar = satisfy (/= '\n') <|> (newline <* notFollowedBy' blankline)

mathDisplay :: (HasReaderOptions st, Stream s m Char, UpdateSourcePos s Char)
            => ParsecT s st m Text
mathDisplay =
      (guardEnabled Ext_tex_math_dollars >> mathWith "$$" "$$")
  <|> (guardEnabled Ext_tex_math_single_backslash >> mathWith "\\[" "\\]")
  <|> (guardEnabled Ext_tex_math_double_backslash >> mathWith "\\\\[" "\\\\]")

mathInline :: (HasReaderOptions st, Stream s m Char, UpdateSourcePos s Char)
           => ParsecT s st m Text
mathInline =
      (guardEnabled Ext_tex_math_dollars >> mathWith "$" "$")
  <|> (guardEnabled Ext_tex_math_single_backslash >> mathWith "\\(" "\\)")
  <|> (guardEnabled Ext_tex_math_double_backslash >> mathWith "\\\\(" "\\\\)")