module Text.TeXMath.Readers.TeX.Macros
( Macro
, parseMacroDefinitions
, applyMacros
)
where
import Data.Char (isDigit, isLetter)
import Control.Monad
import Text.ParserCombinators.Parsec
data Macro = Macro { macroDefinition :: String
, macroParser :: forall st . GenParser Char st String }
instance Show Macro where
show m = "Macro " ++ show (macroDefinition m)
parseMacroDefinitions :: String -> ([Macro], String)
parseMacroDefinitions s =
case parse pMacroDefinitions "input" s of
Left _ -> ([], s)
Right res -> res
pMacroDefinitions :: GenParser Char st ([Macro], String)
pMacroDefinitions = do
pSkipSpaceComments
defs <- sepEndBy pMacroDefinition pSkipSpaceComments
rest <- getInput
return (reverse defs, rest)
pMacroDefinition :: GenParser Char st Macro
pMacroDefinition = newcommand <|> declareMathOperator
pSkipSpaceComments :: GenParser Char st ()
pSkipSpaceComments = spaces >> skipMany (comment >> spaces)
applyMacros :: [Macro] -> String -> String
applyMacros [] s = s
applyMacros ms s =
maybe s id $ iterateToFixedPoint ((2 * length ms) + 1) (applyMacrosOnce ms) s
iterateToFixedPoint :: Eq a => Int -> (a -> Maybe a) -> a -> Maybe a
iterateToFixedPoint 0 _ _ = Nothing
iterateToFixedPoint limit f x =
case f x of
Nothing -> Nothing
Just y
| y == x -> Just y
| otherwise -> iterateToFixedPoint (limit 1) f y
applyMacrosOnce :: [Macro] -> String -> Maybe String
applyMacrosOnce ms s =
case parse (many tok) "input" s of
Right r -> Just $ concat r
Left _ -> Nothing
where tok = try $ do
skipComment
choice [ choice (map macroParser ms)
, ctrlseq
, count 1 anyChar ]
ctrlseq :: GenParser Char st String
ctrlseq = do
char '\\'
res <- many1 letter <|> count 1 anyChar
return $ '\\' : res
newcommand :: GenParser Char st Macro
newcommand = try $ do
char '\\'
try (string "newcommand")
<|> try (string "renewcommand")
<|> string "providecommand"
pSkipSpaceComments
name <- inbraces <|> ctrlseq
guard (take 1 name == "\\")
let name' = drop 1 name
pSkipSpaceComments
numargs <- numArgs
pSkipSpaceComments
optarg <- if numargs > 0
then optArg
else return Nothing
let numargs' = case optarg of
Just _ -> numargs 1
Nothing -> numargs
pSkipSpaceComments
body <- inbraces <|> ctrlseq
let defn = "\\newcommand{" ++ name ++ "}" ++
(if numargs > 0 then ("[" ++ show numargs ++ "]") else "") ++
case optarg of { Nothing -> ""; Just x -> "[" ++ x ++ "]"} ++
"{" ++ body ++ "}"
return $ Macro defn $ try $ do
char '\\'
string name'
when (all isLetter name') $
notFollowedBy letter
pSkipSpaceComments
opt <- case optarg of
Nothing -> return Nothing
Just _ -> liftM (`mplus` optarg) optArg
args <- count numargs' (pSkipSpaceComments >>
(inbraces <|> ctrlseq <|> count 1 anyChar))
let args' = case opt of
Just x -> x : args
Nothing -> args
return $ apply args' $ "{" ++ body ++ "}"
declareMathOperator :: GenParser Char st Macro
declareMathOperator = try $ do
string "\\DeclareMathOperator"
pSkipSpaceComments
star <- option "" (string "*")
pSkipSpaceComments
name <- inbraces <|> ctrlseq
guard (take 1 name == "\\")
let name' = drop 1 name
pSkipSpaceComments
body <- inbraces <|> ctrlseq
let defn = "\\DeclareMathOperator" ++ star ++ "{" ++ name ++ "}" ++
"{" ++ body ++ "}"
return $ Macro defn $ try $ do
char '\\'
string name'
when (all isLetter name') $
notFollowedBy letter
pSkipSpaceComments
return $ "\\operatorname" ++ star ++ "{" ++ body ++ "}"
apply :: [String] -> String -> String
apply args ('#':d:xs) | isDigit d =
let argnum = read [d]
in if length args >= argnum
then args !! (argnum 1) ++ apply args xs
else '#' : d : apply args xs
apply args ('\\':'#':xs) = '\\':'#' : apply args xs
apply args (x:xs) = x : apply args xs
apply _ "" = ""
skipComment :: GenParser Char st ()
skipComment = skipMany comment
comment :: GenParser Char st ()
comment = do
char '%'
skipMany (notFollowedBy newline >> anyChar)
newline
return ()
numArgs :: GenParser Char st Int
numArgs = option 0 $ do
pSkipSpaceComments
char '['
pSkipSpaceComments
n <- digit
pSkipSpaceComments
char ']'
return $ read [n]
optArg :: GenParser Char st (Maybe String)
optArg = option Nothing $ (liftM Just $ inBrackets)
escaped :: String -> GenParser Char st String
escaped xs = try $ char '\\' >> oneOf xs >>= \x -> return ['\\',x]
inBrackets :: GenParser Char st String
inBrackets = try $ do
char '['
pSkipSpaceComments
res <- manyTill (skipComment >> (escaped "[]" <|> count 1 anyChar))
(try $ pSkipSpaceComments >> char ']')
return $ concat res
inbraces :: GenParser Char st String
inbraces = try $ do
char '{'
res <- manyTill (skipComment >> (inbraces' <|> count 1 anyChar <|> escaped "{}"))
(try $ skipComment >> char '}')
return $ concat res
inbraces' :: GenParser Char st String
inbraces' = do
res <- inbraces
return $ '{' : (res ++ "}")