{-# LANGUAGE GeneralizedNewtypeDeriving, ViewPatterns, GADTs, OverloadedStrings #-} {- Copyright (C) 2014 Matthew Pickering This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -} module Text.TeXMath.Writers.TeX (writeTeX, writeTeXWith, addLaTeXEnvironment) where import Text.TeXMath.Types import Text.TeXMath.Unicode.ToTeX (getTeXMath) import Text.TeXMath.Unicode.ToUnicode (fromUnicode) import qualified Text.TeXMath.Shared as S import qualified Data.Text as T import Data.Generics (everywhere, mkT) import Control.Monad (when, unless, foldM_) import Control.Monad.Reader (MonadReader, runReader, Reader, asks, local) import Control.Monad.Writer( MonadWriter, WriterT, execWriterT, tell, censor) import Text.TeXMath.TeX import Data.Either (isRight) -- import Debug.Trace -- tr' x = trace (show x) x -- | Transforms an expression tree to equivalent LaTeX with the default -- packages (amsmath and amssymb) writeTeX :: [Exp] -> T.Text writeTeX = writeTeXWith defaultEnv -- | Adds the correct LaTeX environment around a TeXMath fragment addLaTeXEnvironment :: DisplayType -> T.Text -> T.Text addLaTeXEnvironment dt math = case dt of DisplayInline -> "\\(" <> math <> "\\)" DisplayBlock -> "\\[" <> math <> "\\]" -- | Transforms an expression tree to equivalent LaTeX with the specified -- packages writeTeXWith :: Env -> [Exp] -> T.Text writeTeXWith env es = T.drop 1 . T.init . flip renderTeX "" . Grouped $ runExpr env $ mapM_ writeExp (removeOuterGroup es) runExpr :: Env -> Math () -> [TeX] runExpr e m = flip runReader (MathState e False) $ execWriterT (runTeXMath m) data MathState = MathState{ mathEnv :: Env , mathConvertible :: Bool } deriving Show setConvertible :: MathState -> MathState setConvertible s = s{ mathConvertible = True } newtype Math a = Math {runTeXMath :: WriterT [TeX] (Reader MathState) a} deriving (Functor, Applicative, Monad, MonadReader MathState , MonadWriter [TeX]) getTeXMathM :: T.Text -> Math [TeX] getTeXMathM s = getTeXMath s <$> asks mathEnv tellGroup :: Math () -> Math () tellGroup = censor ((:[]) . Grouped) tellGenFrac :: T.Text -> T.Text -> Math () tellGenFrac open close = tell [ ControlSeq "\\genfrac" , Grouped [Literal open] , Grouped [Literal close] , Grouped [Literal "0pt"] , Grouped [] ] writeBinom :: T.Text -> Exp -> Exp -> Math () writeBinom cmd x y = do env <- asks mathEnv if "amsmath" `elem` env then do case cmd of "\\choose" -> tell [ControlSeq "\\binom"] "\\brack" -> tellGenFrac "[" "]" "\\brace" -> tellGenFrac "\\{" "\\}" "\\bangle" -> tellGenFrac "\\langle" "\\rangle" _ -> error "writeBinom: unknown cmd" tellGroup $ writeExp x tellGroup $ writeExp y else tellGroup $ do writeExp x tell [ControlSeq cmd] writeExp y writeExp :: Exp -> Math () writeExp (ENumber s) = tell =<< getTeXMathM s writeExp (EGrouped es) = tellGroup (mapM_ writeExp es) writeExp (EDelimited "(" ")" [Right (EFraction NoLineFrac x y)]) = writeBinom "\\choose" x y writeExp (EDelimited "[" "]" [Right (EFraction NoLineFrac x y)]) = do writeBinom "\\brack" x y writeExp (EDelimited "{" "}" [Right (EFraction NoLineFrac x y)]) = do writeBinom "\\brace" x y writeExp (EDelimited "\x27E8" "\x27E9" [Right (EFraction NoLineFrac x y)]) = do writeBinom "\\bangle" x y writeExp (EDelimited open close [Right (EFraction NoLineFrac x y)]) = do writeExp (EDelimited open close [Right (EArray [AlignCenter] [[[x]],[[y]]])]) writeExp (EDelimited open close [Right (EArray aligns rows)]) = do env <- asks mathEnv case ("amsmath" `elem` env, open, close) of (True, "{", "") | aligns == [AlignLeft, AlignLeft] -> table "cases" [] rows (True, "(", ")") | all (== AlignCenter) aligns -> table "pmatrix" [] rows (True, "[", "]") | all (== AlignCenter) aligns -> table "bmatrix" [] rows (True, "{", "}") | all (== AlignCenter) aligns -> table "Bmatrix" [] rows (True, "\x2223", "\x2223") | all (== AlignCenter) aligns -> table "vmatrix" [] rows (True, "\x2225", "\x2225") | all (== AlignCenter) aligns -> table "Vmatrix" [] rows _ -> do writeDelim DLeft open writeExp (EArray aligns rows) writeDelim DRight close -- this clause is meant to pair with delimitedImplicit in the tex reader: writeExp (EDelimited open close es) | all isStandardHeight es , open == "(" || open == "[" || open == "|" , close == ")" || close == "]" || close == "|" , all isRight es = do getTeXMathM open >>= tell mapM_ (either (writeDelim DMiddle) writeExp) es getTeXMathM close >>= tell where isStandardHeight (Right (EIdentifier{})) = True isStandardHeight (Right (ENumber{})) = True isStandardHeight (Right (ESpace{})) = True isStandardHeight (Right (ESymbol ty _)) = ty `elem` [Ord, Op, Bin, Rel, Pun] isStandardHeight _ = False writeExp (EDelimited open close es) = do writeDelim DLeft open mapM_ (either (writeDelim DMiddle) writeExp) es writeDelim DRight close writeExp (EIdentifier s) = do math <- getTeXMathM s case math of [] -> return () [t] -> tell [t] ts -> tell [Grouped ts] writeExp o@(EMathOperator s) = do math <- getTeXMathM s case S.getOperator o of Just op -> tell [op] Nothing -> do tell [ControlSeq "\\operatorname"] -- use \operatorname* if convertible asks mathConvertible >>= flip when (tell [Token '*']) tell [Grouped math] writeExp (ESymbol Ord (T.unpack -> [c])) -- do not render "invisible operators" | c `elem` ['\x2061'..'\x2064'] = return () -- see 3.2.5.5 of mathml spec writeExp (ESymbol t s) = do s' <- getTeXMathM s when (t == Bin || t == Rel) $ tell [Space] if T.length s > 1 && (t == Bin || t == Rel || t == Op) then tell [ControlSeq ("\\math" <> T.toLower (tshow t)), Grouped [ControlSeq "\\text", Grouped s']] else tell s' when (t == Bin || t == Rel) $ tell [Space] writeExp (ESpace width) = do env <- asks mathEnv tell [ControlSeq $ getSpaceCommand ("amsmath" `elem` env) width] writeExp (EFraction fractype e1 e2) = do let cmd = case fractype of NormalFrac -> "\\frac" DisplayFrac -> "\\dfrac" InlineFrac -> "\\tfrac" NoLineFrac -> "\\binom" -- shouldn't happen because -- a binom will be in a delimited tell [ControlSeq cmd] tellGroup (writeExp e1) tellGroup (writeExp e2) writeExp (ESub b e1) = do (if isFancy b then tellGroup else id) $ writeExp b tell [Token '_'] tellGroup (writeExp e1) writeExp (ESuper b e1) = do (if isFancy b then tellGroup else id) $ writeExp b tell [Token '^'] tellGroup (writeExp e1) writeExp (ESubsup b e1 e2) = do (if isFancy b then tellGroup else id) $ writeExp b tell [Token '_'] tellGroup (writeExp e1) tell [Token '^'] tellGroup (writeExp e2) writeExp (EOver convertible b e1) = do env <- asks mathEnv case xarrow b of Just arrowCtrlSeq | "amsmath" `elem` env -> do tell [arrowCtrlSeq] tellGroup (writeExp e1) _ -> writeScript Over convertible b e1 writeExp (EUnder convertible b e1) = writeScript Under convertible b e1 writeExp (EUnderover convertible b e1@(ESymbol Accent _) e2) = writeExp (EUnder convertible (EOver False b e2) e1) writeExp (EUnderover convertible b e1 e2@(ESymbol Accent _)) = writeExp (EOver convertible (EUnder False b e1) e2) writeExp (EUnderover convertible b e1 e2) = do env <- asks mathEnv case xarrow b of Just arrowCtrlSeq | "amsmath" `elem` env -> do tell [arrowCtrlSeq] tell [Token '['] tellGroup $ writeExp e1 tell [Token ']'] tellGroup (writeExp e2) Nothing | isOperator b -> do (if isFancy b then tellGroup else id) $ (if convertible then local setConvertible else id) $ writeExp b unless convertible $ tell [ControlSeq "\\limits"] tell [Token '_'] tellGroup (checkSubstack e1) tell [Token '^'] tellGroup (checkSubstack e2) _ -> writeExp (EUnder convertible (EOver convertible b e2) e1) writeExp (ESqrt e) = do tell [ControlSeq "\\sqrt"] tellGroup (writeExp e) writeExp (ERoot i e) = do tell [ControlSeq "\\sqrt"] tell [Token '['] writeExp i tell [Token ']'] tellGroup (writeExp e) writeExp (EPhantom e) = do tell [ControlSeq "\\phantom"] tellGroup (writeExp e) writeExp (EBoxed e) = do env <- asks mathEnv if "amsmath" `elem` env then do tell [ControlSeq "\\boxed"] tellGroup (writeExp e) else writeExp e writeExp (EScaled size e) | case e of (ESymbol Open _) -> True (ESymbol Close _) -> True _ -> False = do case S.getScalerCommand size of Just s -> tell [ControlSeq s] Nothing -> return () writeExp e | otherwise = writeExp e writeExp (EText ttype s) = do let txtcmd = getTextCommand ttype let fixSpace (Literal "\\ ") = Literal " " fixSpace x = x case map (fixSpace . escapeLaTeX) (T.unpack s) of [] -> return () xs -> tell $ txtcmd (Grouped xs) writeExp (EStyled ttype es) = do txtcmd <- (flip S.getLaTeXTextCommand ttype) <$> asks mathEnv tell [ControlSeq txtcmd] tellGroup (mapM_ writeExp $ everywhere (mkT (fromUnicode ttype)) es) writeExp (EArray as rows) | isRLSequence as = do env <- asks mathEnv if "amsmath" `elem` env then table "aligned" [] rows else table "array" as rows writeExp (EArray aligns rows) = do env <- asks mathEnv if "amsmath" `elem` env && all (== AlignCenter) aligns then table "matrix" [] rows else table "array" aligns rows isRLSequence :: [Alignment] -> Bool isRLSequence [AlignRight, AlignLeft] = True isRLSequence (AlignRight : AlignLeft : as) = isRLSequence as isRLSequence _ = False table :: T.Text -> [Alignment] -> [ArrayLine] -> Math () table name aligns rows = do tell [ControlSeq "\\begin", Grouped [Literal name]] unless (null aligns) $ tell [Grouped [Literal columnAligns]] tell [Token '\n'] doRows rows tell [ControlSeq "\\end", Grouped [Literal name]] where columnAligns = T.pack $ map alignmentToLetter aligns alignmentToLetter AlignLeft = 'l' alignmentToLetter AlignCenter = 'c' alignmentToLetter AlignRight = 'r' doRows :: [ArrayLine] -> Math () doRows [] = return () doRows ([]:[]) = tell [Token '\n'] doRows ([]:ls) = tell [Space, Literal "\\\\", Token '\n'] >> doRows ls doRows ([c]:ls) = cell c >> doRows ([]:ls) doRows ((c:cs):ls) = cell c >> tell [Space, Token '&', Space] >> doRows (cs:ls) cell :: [Exp] -> Math () cell = mapM_ writeExp data FenceType = DLeft | DMiddle | DRight type Delim = T.Text writeDelim :: FenceType -> Delim -> Math () writeDelim fence delim = do tex <- getTeXMathM delim valid <- elem tex <$> delimiters nullLim <- getTeXMathM "." let delimCmd = if valid then tex else nullLim tell $ case fence of DLeft -> [ControlSeq "\\left"] ++ delimCmd ++ [Space] ++ if valid then [] else tex DMiddle -> case valid of True -> [Space] ++ [ControlSeq "\\middle"] ++ tex ++ [Space] False -> tex DRight -> [Space, ControlSeq "\\right"] ++ delimCmd ++ if valid then [] else tex writeScript :: Position -> Bool -> Exp -> Exp -> Math () writeScript pos convertible b e1 = do let diacmd = case e1 of ESymbol stype a | stype `elem` [Accent, TOver, TUnder] -> S.getDiacriticalCommand pos a _ -> Nothing case diacmd of Just cmd -> do tell [ControlSeq cmd] tellGroup (writeExp b) Nothing | isOperator b -> do (if isFancy b then tellGroup else id) $ (if convertible then local setConvertible else id) $ writeExp b unless convertible $ tell [ControlSeq "\\limits"] tell [Token $ case pos of { Over -> '^'; Under -> '_' }] tellGroup (checkSubstack e1) | case pos of {Over -> True; _ -> False} , e1 == ESymbol Accent "\831" -> do -- double bar tell [ControlSeq "\\overline", Literal "{", ControlSeq "\\overline"] tellGroup (writeExp b) tell [Literal "}"] | otherwise -> do case pos of Over -> tell [ControlSeq "\\overset"] Under -> tell [ControlSeq "\\underset"] tellGroup (writeExp e1) tellGroup (writeExp b) -- Replace an array with a substack if appropriate. checkSubstack :: Exp -> Math () checkSubstack e@(EArray [AlignCenter] rows) = do env <- asks mathEnv if "amsmath" `elem` env then do tell [ControlSeq "\\substack"] tellGroup $ foldM_ (\first r -> do if first then return () else tell [Space, Literal "\\\\", Space] mapM_ (mapM_ writeExp . removeOuterGroup) r return False) True rows else writeExp e checkSubstack e = writeExp e -- Utility -- | Maps a length in em to the nearest LaTeX space command getSpaceCommand :: Bool -> Rational -> T.Text getSpaceCommand amsmath width = case floor (width * 18) :: Int of -3 -> "\\!" 0 -> "" 3 -> "\\," 4 -> "\\ " -- could also use \: or \> 5 -> "\\;" 18 -> "\\quad" 36 -> "\\qquad" n -> if amsmath then "\\mspace{" <> tshow n <> "mu}" else "{\\mskip " <> tshow n <> "mu}" getTextCommand :: TextType -> TeX -> [TeX] getTextCommand tt x = case tt of TextNormal -> [ControlSeq "\\text", x] TextItalic -> [ControlSeq "\\textit", x] TextBold -> [ControlSeq "\\textbf", x] TextMonospace -> [ControlSeq "\\texttt", x] TextBoldItalic -> [ControlSeq "\\textit", Grouped [ControlSeq "\\textbf", x]] TextSansSerif -> [ControlSeq "\\textsf", x] TextSansSerifBold -> [ControlSeq "\\textbf", Grouped [ControlSeq "\\textsf", x]] TextSansSerifItalic -> [ControlSeq "\\textit", Grouped [ControlSeq "\\textsf", x]] TextSansSerifBoldItalic -> [ControlSeq "\\textbf", Grouped [ControlSeq "\\textit", Grouped [ControlSeq "\\textsf", x]]] _ -> [ControlSeq "\\text", x] xarrow :: Exp -> Maybe TeX xarrow (ESymbol Op "\x2190") = Just $ ControlSeq "\\xleftarrow" xarrow (ESymbol Op "\x2192") = Just $ ControlSeq "\\xrightarrow" xarrow _ = Nothing -- Commands which can be used with \left and \right delimiters :: Math [[TeX]] delimiters = do env <- asks mathEnv let commands' = [ ".", "(", ")", "[", "]", "|", "\x2016", "{", "}" , "\x2309", "\x2308", "\x2329", "\x232A" , "\x230B", "\x230A", "\x231C", "\x231D"] return $ filter (not . null) (map (flip getTeXMath env) commands') isFancy :: Exp -> Bool isFancy (ESub _ _) = True isFancy (ESuper _ _) = True isFancy (ESubsup _ _ _) = True isFancy (EOver _ _ _) = True isFancy (EUnder _ _ _) = True isFancy (EUnderover _ _ _ _) = True isFancy (ESqrt _) = True isFancy (ERoot _ _) = True isFancy (EPhantom _) = True isFancy _ = False isOperator :: Exp -> Bool isOperator (EMathOperator _) = True isOperator (ESymbol Op _) = True isOperator _ = False removeOuterGroup :: [Exp] -> [Exp] removeOuterGroup [EGrouped es] = es removeOuterGroup es = es tshow :: Show a => a -> T.Text tshow = T.pack . show