content: {-# LANGUAGE TupleSections #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-
  Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
  
  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
  -}
  
  {- | Functions for parsing a LaTeX formula to a Haskell representation.
  -}
  
  module Text.TeXMath.Readers.TeX (readTeX)
  where
  
  import Data.List (intercalate, intersperse, find, foldl')
  import Control.Monad
  import Data.Char (isDigit, isAscii, isLetter)
  import qualified Data.Map as M
  import qualified Data.Text as T
  import Data.Text (Text)
  import Data.Ratio ((%))
  import Data.Maybe (catMaybes, fromJust, mapMaybe)
  import Text.Parsec hiding (label)
  import Text.Parsec.Error
  import Text.Parsec.Text
  import Text.TeXMath.Types
  import Data.Functor (($>))
  import qualified Text.TeXMath.Shared as S
  import Text.TeXMath.Readers.TeX.Macros (applyMacros, parseMacroDefinitions)
  import Text.TeXMath.Unicode.ToTeX (getSymbolType)
  import Text.TeXMath.Unicode.ToUnicode (toUnicode)
  import Text.TeXMath.Shared (getSpaceChars)
  import Data.Generics (everywhere, mkT)
  import Text.TeXMath.Readers.TeX.Commands ( styleOps, textOps, enclosures,
                                             operators, symbols, siUnitMap )
  import Data.Text.Read (decimal)
  
  type TP = Parser
  
  -- The parser
  
  primes :: TP Exp
  primes = do
    ESymbol Pun . getPrimes <$> (many1 (char '\'') <* ignorable)
  
  getPrimes :: [Char] -> Text
  getPrimes cs =
    case cs of
      "" -> ""
      "'" -> "\x2032"
      "''" -> "\x2033"
      "'''" -> "\x2034"
      "''''" -> "\x2057"
      _ -> "\x2057" <> getPrimes (drop 4 cs)
  
  expr1 :: TP Exp
  expr1 = choice
            [ inbraces
            , variable
            , number
            , unicode
            , operator
            , bareSubSup
            , enclosure
            , hyperref
            , command
            ]
  
  -- | Parse a formula, returning a list of 'Exp'.
  readTeX :: Text -> Either Text [Exp]
  readTeX inp =
    let (ms, rest) = parseMacroDefinitions inp in
    either (Left . showParseError inp) (Right . fixBinList)
      $ parse formula "formula" $ applyMacros ms rest
   where
    -- | Convert Bin symbol type in certain contexts (#176, #234).
    fixBinList :: [Exp] -> [Exp]
    fixBinList =
      reverse . foldl' goExp [] . everywhere (mkT fixBins)
  
    -- TeXBook:
    -- 5. If the current item is a Bin atom, and if this was the first
    -- atom in the list, or if the most recent previous atom was Bin, Op,
    -- Rel, Open, or Punct, change the current Bin to Ord and continue with
    -- Rule 14. Otherwise continue with Rule 17.
    -- 6. If the current item is a Rel or Close or Punct atom, and if
    -- the most recent previous atom was Bin, change that previous Bin
    -- to Ord. Continue with Rule 17.
  
    fixBins :: Exp -> Exp
    fixBins e =
      case e of
        EGrouped es
          -> EGrouped (reverse $ foldl' goExp [] es)
        EDelimited op cl des
          -> EDelimited op cl (reverse $ foldl' goInDel [] des)
        EArray als alines
          -> EArray als (map (map (reverse . foldl' goExp [])) alines)
        EStyled tt es
          -> EStyled tt (reverse $ foldl' goExp [] es)
        _ -> e
  
    goExp :: [Exp] -> Exp -> [Exp]
    goExp [] (ESymbol Bin t) = [ESymbol Ord t]
    goExp  accum@(ESymbol ty _ : _) (ESymbol Bin t)
      | ty `elem` [Bin, Op, Rel, Open, Pun]
        = ESymbol Ord t : accum
    goExp (ESymbol Bin t' : rest) (ESymbol ty t)
      | ty `elem` [Rel, Close, Pun]
        = ESymbol ty t : ESymbol Ord t' : rest
    goExp xs x = x : xs
  
    goInDel :: [InEDelimited] -> InEDelimited -> [InEDelimited]
    goInDel [] (Right (ESymbol Bin t)) = [Right (ESymbol Ord t)]
    goInDel accum@(Left _ : _) (Right (ESymbol Bin t))
      = Right (ESymbol Ord t) : accum
    goInDel accum@(Right (ESymbol ty _) : _) (Right (ESymbol Bin t))
      | ty `elem` [Bin, Op, Rel, Open, Pun]
        = Right (ESymbol Ord t) : accum
    goInDel (Right (ESymbol Bin t') : rest) (Right (ESymbol ty t))
      | ty `elem` [Rel, Close, Pun]
        = Right (ESymbol ty t) : Right (ESymbol Ord t') : rest
    goInDel xs x = x : xs
  
  showParseError :: Text -> ParseError -> Text
  showParseError inp pe =
    snippet <> "\n" <> caretline <>
      T.pack (showErrorMessages "or" "unknown" "expecting" "unexpected" "eof"
              (errorMessages pe))
    where errln = sourceLine (errorPos pe)
          errcol = sourceColumn (errorPos pe)
          snipoffset = max 0 (errcol - 20)
          inplns = T.lines inp
          ln = if length inplns >= errln
                  then inplns !! (errln - 1)
                  else ""  -- should not happen
          snippet = T.take 40 $ T.drop snipoffset ln
          caretline = T.replicate (errcol - snipoffset - 1) " " <> "^"
  
  anyCtrlSeq :: TP Text
  anyCtrlSeq = lexeme $ try $ do
    char '\\'
    res <- count 1 (satisfy (not . isLetter)) <|> many1 (satisfy isLetter)
    return $ T.pack $ '\\' : res
  
  ctrlseq :: String -> TP String
  ctrlseq s = lexeme $ try $ do
    result <- string ('\\':s)
    case s of
         [c] | not (isLetter c) -> return ()
         _ -> (do pos <- getPosition
                  letter
                  setPosition pos
                  mzero <?> ("non-letter after \\" ++ s))
              <|> return ()
    return result
  
  ignorable :: TP ()
  ignorable = skipMany $
          comment
      <|> label
      <|> tag
      <|> () <$ ctrlseq "nonumber"
      <|> (skipMany1 space <?> "whitespace")
      <|> (() <$ ctrlseq "allowbreak")
  
  comment :: TP ()
  comment = char '%' *> skipMany (noneOf "\n") *> optional newline
  
  label :: TP ()
  label = ctrlseq "label" *> braces (skipMany (noneOf "}"))
  
  tag :: TP ()
  tag = ctrlseq "tag" *> optional (char '*') *> braces (skipMany (noneOf "}"))
  
  unGrouped :: Exp -> [Exp]
  unGrouped (EGrouped xs) = xs
  unGrouped x = [x]
  
  formula :: TP [Exp]
  formula = unGrouped <$> (ignorable *> manyExp expr <* eof)
  
  expr :: TP Exp
  expr = do
    optional (ctrlseq "displaystyle" <|> ctrlseq "textstyle" <|>
              ctrlseq "scriptstyle" <|> ctrlseq "scriptscriptstyle")
    (a, convertible) <- try (braces operatorname) -- needed because macros add {}
                   <|> operatorname
                   <|> ((,False) <$> expr1)
    limits <- limitsIndicator
    subSup limits convertible a <|> superOrSubscripted limits convertible a <|> return a
  
  hyperref :: TP Exp
  hyperref = do
    ctrlseq "hyperref"  -- we just ignore hyperref, see #186
    optional inbrackets
    inbraces
  
  command :: TP Exp
  command = try $ do
    c <- anyCtrlSeq
    guard $ c /= "\\end" -- handled in environment
         && c /= "\\operatorname" -- handled in expr
    choice
      [ text c
      , styled c
      , colored c
      , root c
      , xspace c
      , mathop c
      , phantom c
      , boxed c
      , cancel c
      , binary c
      , genfrac c
      , substack c
      , environment c
      , ensuremath c
      , scaled c
      , negated c
      , siunitx c
      , arrow c
      , tSymbol c
      ] <|> unexpected ("control sequence " <> T.unpack c)
  
  
  -- | Parser for \operatorname command.
  -- Returns a tuple of EMathOperator name and Bool depending on the flavor
  -- of the command:
  --
  --     - True for convertible operator (\operator*)
  --
  --     - False otherwise
  operatorname :: TP (Exp, Bool)
  operatorname = do
      ctrlseq "operatorname"
      -- these are slightly different but we won't worry about that here...
      convertible <- (char '*' >> spaces >> return True) <|> return False
      tok' <- texSymbol <|> braces (manyExp expr1) <|> texChar
      tok'' <- (EMathOperator <$> (expToOperatorName tok'))
             <|> return (EStyled TextNormal [tok'])
      return (tok'', convertible)
  
  -- | Converts identifiers, symbols and numbers to a flat string.
  -- Fails if the expression contains anything else.
  expToOperatorName :: MonadPlus m => Exp -> m Text
  expToOperatorName e = case e of
              EGrouped xs -> T.concat <$> mapM (toStr TextNormal) xs
              _ -> toStr TextNormal e
      where
            toStr sty (EIdentifier s)     = return $ toUnicode sty s
            toStr _   (EText sty' s)      = return $ toUnicode sty' s
            toStr sty (ENumber s)         = return $ toUnicode sty s
            toStr sty (EMathOperator s)   = return $ toUnicode sty s
            -- handle special characters
            toStr _ (ESymbol _ "\x2212")  = return "-"
            toStr _ (ESymbol _ "\x2032")  = return "'"
            toStr _ (ESymbol _ "\x2033")  = return "''"
            toStr _ (ESymbol _ "\x2034")  = return "'''"
            toStr _ (ESymbol _ "\x2057")  = return "''''"
            toStr _ (ESymbol _ "\x02B9")  = return "'"
            toStr sty (ESymbol _ s)       = return $ toUnicode sty s
            toStr _ (ESpace n)            = return $ getSpaceChars n
            toStr _ (EStyled sty' exps)   = T.concat <$>
                                              sequence (map (toStr sty') exps)
            toStr _   _                   = mzero
  
  bareSubSup :: TP Exp
  bareSubSup = subSup Nothing False (EIdentifier "")
    <|> superOrSubscripted Nothing False (EIdentifier "")
  
  limitsIndicator :: TP (Maybe Bool)
  limitsIndicator =
     (ctrlseq "limits" >> return (Just True))
    <|> (ctrlseq "nolimits" >> return (Just False))
    <|> return Nothing
  
  binomCmd :: TP Text
  binomCmd = oneOfCommands (M.keys binomCmds)
  
  binomCmds :: M.Map Text (Exp -> Exp -> Exp)
  binomCmds = M.fromList
              [ ("\\choose", \x y ->
                  EDelimited "(" ")" [Right (EFraction NoLineFrac x y)])
              , ("\\brack", \x y ->
                  EDelimited "[" "]" [Right (EFraction NoLineFrac x y)])
              , ("\\brace", \x y ->
                  EDelimited "{" "}" [Right (EFraction NoLineFrac x y)])
              , ("\\bangle", \x y ->
                  EDelimited "\x27E8" "\x27E9" [Right (EFraction NoLineFrac x y)])
              ]
  
  genfrac :: Text -> TP Exp
  genfrac "\\genfrac" = do
    let opener = option "" $
                  T.singleton <$> ((char '\\' >> anyChar) <|> anyChar)
    let closer = option "" $
                  T.singleton <$> ((char '\\' >> anyChar) <|> anyChar)
    openDelim <- braces opener <|> opener
    closeDelim <- braces closer <|> closer
    bar <- False <$ try (braces (string "0pt")) <|> True <$ texToken
    displayStyle <- True <$ try (braces (char '0')) <|> False <$ texToken
    x <- texToken
    y <- texToken
    let fracType = case (bar, displayStyle) of
                        (False, _)   -> NoLineFrac
                        (True, True) -> DisplayFrac
                        _            -> NormalFrac
    return $ EDelimited openDelim closeDelim
                        [Right (EFraction fracType x y)]
  genfrac _ = mzero
  
  substack :: Text -> TP Exp
  substack "\\substack" = do
    formulas <- braces $ ignorable >> (manyExp expr) `sepEndBy` endLine
    return $ EArray [AlignCenter] $ map (\x -> [[x]]) formulas
  substack _ = mzero
  
  asGroup :: [Exp] -> Exp
  asGroup [x] = x
  asGroup xs = EGrouped xs
  
  -- variant of many that is sensitive to \choose and other such commands
  manyExp' :: Bool -> TP Exp -> TP Exp
  manyExp' requireNonempty p = do
    initial <- if requireNonempty
                  then many1 (notFollowedBy binomCmd >> p)
                  else many (notFollowedBy binomCmd >> p)
    let withCmd :: Text -> TP Exp
        withCmd cmd =
           case M.lookup cmd binomCmds of
                Just f  -> f <$> (asGroup <$> pure initial)
                             <*> (asGroup <$> many p)
                Nothing -> fail $ "Unknown command " <> T.unpack cmd
    (binomCmd >>= withCmd) <|> return (asGroup initial)
  
  manyExp :: TP Exp -> TP Exp
  manyExp = manyExp' False
  
  many1Exp :: TP Exp -> TP Exp
  many1Exp = manyExp' True
  
  inbraces :: TP Exp
  inbraces = braces (ignorable *> manyExp expr)
  
  texToken :: TP Exp
  texToken = texSymbol <|> inbraces <|> texChar
  
  -- Remove superfluous EGrouped if present.
  deGroup :: Exp -> Exp
  deGroup (EGrouped [x]) = x
  deGroup x = x
  
  texChar :: TP Exp
  texChar =
    do
      c <- noneOf "\n\t\r \\{}" <* spaces
      return $ (if isDigit c then ENumber else EIdentifier) $ T.singleton c
  
  inbrackets :: TP Exp
  inbrackets = (brackets $ manyExp $ notFollowedBy (char ']') >> expr)
  
  number :: TP Exp
  number = lexeme $ ENumber <$> try decimalNumber
    where decimalNumber = do
            xs <- many digit
            ys <- option [] $ try (char '.' >> (('.':) <$> many1 digit))
            case xs ++ ys of
                 []  -> mzero
                 zs  -> return $ T.pack zs
  
  enclosure :: TP Exp
  enclosure = delimited <|> delimitedImplicit <|> basicEnclosure
  
  basicEnclosure :: TP Exp
  basicEnclosure = try $ do
    possibleEncl <- lexeme (anyCtrlSeq <|> countChar 1 (oneOf "()[]|"))
    case M.lookup possibleEncl enclosures of
         Just x  -> return x
         Nothing -> mzero
  
  fence :: String -> TP Text
  fence cmd = do
    symbol cmd
    let nullDelim = try (ESymbol Open "" <$ symbol ".")
        angleDelim = try $ choice
          [ ESymbol Open "\x27E8" <$ symbol "<"
          , ESymbol Close "\x27E9" <$ symbol ">"
          ]
    enc <- basicEnclosure <|> nullDelim <|> angleDelim
    case enc of
         ESymbol Open x  -> return x
         ESymbol Close x -> return x
         _ -> mzero
  
  middle :: TP Text
  middle = fence "\\middle"
  
  right :: TP Text
  right = fence "\\right"
  
  delimited :: TP Exp
  delimited = do
    openc <- try $ fence "\\left"
    contents <- concat <$>
                many (try $ ((:[]) . Left  <$> middle)
                        <|> (map Right . unGrouped <$>
                               many1Exp (notFollowedBy right *> expr)))
    closec <- right <|> return ""
    return $ EDelimited openc closec contents
  
  delimitedImplicit :: TP Exp
  delimitedImplicit = try $ do
    (opent, closer) <- (("\x2016", "\x2016" <$ ctrlseq "rVert") <$ ctrlseq "lVert")
    contents <- concat <$>
                many (try $ ((:[]) . Left  <$> middle)
                        <|> (map Right . unGrouped <$>
                               many1Exp (notFollowedBy closer *> expr)))
    closet <- T.pack <$> closer
    return $ EDelimited opent closet contents
  
  scaled :: Text -> TP Exp
  scaled cmd = do
    case S.getScalerValue cmd of
         Just r  -> EScaled r <$> (basicEnclosure <|> operator)
         Nothing -> mzero
  
  endLine :: TP Char
  endLine = try $ do
    symbol "\\\\"
    optional inbrackets  -- can contain e.g. [1.0in] for a line height, not yet supported
    return '\n'
  
  -- Within environments provided by AMSmath, spaces are not allowed between
  -- the double-backslash command and its optional argument.
  endLineAMS :: TP Char
  endLineAMS = lexeme $ try $ do
    string "\\\\"
    skipMany comment
    optional inbrackets  -- can contain e.g. [1.0in] for a line height, not yet supported
    return '\n'
  
  arrayLine :: TP ArrayLine
  arrayLine =
    sepBy1
      (ignorable' *>
        (unGrouped <$>
          manyExp (try (notFollowedBy (('\n' <$ ctrlseq "end") <|> endLine)) *>
                     expr <* ignorable'))) (symbol "&")
    where ignorable' = ignorable >> optional (try (ctrlseq "hline" >> ignorable'))
    -- we don't represent the line, but it shouldn't crash parsing
  
  arrayAlignments :: TP [Alignment]
  arrayAlignments = mconcat <$>
    braces (many (
                  ((:[]) . letterToAlignment <$> letter)
              <|> ([] <$ char '|')
              <|> ([] <$ oneOf " \t")
              <|> ([] <$ ((char '@' <|> char '!') <* inbraces))
              <|> (do char '*'
                      num <- T.pack <$> braces (many1 digit)
                      cols <- arrayAlignments
                      case decimal num of
                        Left msg -> fail msg
                        Right (n :: Int, _)
                                 -> return $ mconcat $ replicate n cols)
                 ))
   where
     letterToAlignment 'l' = AlignLeft
     letterToAlignment 'c' = AlignCenter
     letterToAlignment 'r' = AlignRight
     letterToAlignment _   = AlignCenter
  
  environment :: Text -> TP Exp
  environment "\\begin" = do
    name <- braces (oneOfStrings (M.keys environments) <* optional (char '*'))
    spaces
    case M.lookup name environments of
          Just env -> do
            result <- env
            spaces
            ctrlseq "end"
            braces (textStr name <* optional (char '*'))
            spaces
            return result
          Nothing  -> mzero  -- should not happen
  environment _ = mzero
  
  environments :: M.Map Text (TP Exp)
  environments = M.fromList
    [ ("array", stdarray)
    , ("eqnarray", eqnarray)
    , ("align", align)
    , ("aligned", align)
    , ("alignat", inbraces *> spaces *> align)
    , ("alignedat", inbraces *> spaces *> align)
    , ("flalign", flalign)
    , ("flaligned", flalign)
    , ("cases", cases)
    , ("matrix", matrixWith "" "")
    , ("smallmatrix", matrixWith "" "")
    , ("pmatrix", matrixWith "(" ")")
    , ("bmatrix", matrixWith "[" "]")
    , ("Bmatrix", matrixWith "{" "}")
    , ("vmatrix", matrixWith "\x2223" "\x2223")
    , ("Vmatrix", matrixWith "\x2225" "\x2225")
    , ("split", align)
    , ("multline", gather)
    , ("multlined", gather)
    , ("gather", gather)
    , ("gathered", gather)
    , ("equation", equation)
    ]
  
  alignsFromRows :: Alignment -> [ArrayLine] -> [Alignment]
  alignsFromRows _ [] = []
  alignsFromRows defaultAlignment (r:_) = replicate (length r) defaultAlignment
  
  matrixWith :: Text -> Text -> TP Exp
  matrixWith opendelim closedelim = do
    lines' <- sepEndBy arrayLine endLineAMS
    let aligns = alignsFromRows AlignCenter lines'
    return $ if T.null opendelim && T.null closedelim
                then EArray aligns lines'
                else EDelimited opendelim closedelim
                         [Right $ EArray aligns lines']
  
  stdarray :: TP Exp
  stdarray = do
    aligns <- arrayAlignments
    lines' <- sepEndBy1 arrayLine endLine
    return $ EArray aligns lines'
  
  gather :: TP Exp
  gather = do
    rows <- sepEndBy arrayLine endLineAMS
    return $ EArray (alignsFromRows AlignCenter rows) rows
  
  equation :: TP Exp
  equation = do
    notFollowedBy (ctrlseq "end" >> return '\n')
    manyExp (notFollowedBy endLine >> expr)
  
  eqnarray :: TP Exp
  eqnarray = do
    rows <- sepEndBy1 arrayLine endLine
    let n = maximum $ map length rows
    return $ EArray (take n $ cycle [AlignRight, AlignCenter, AlignLeft]) rows
  
  align :: TP Exp
  align = do
    rows <- sepEndBy1 arrayLine endLineAMS
    let n = maximum $ map length rows
    return $ EArray (take n $ cycle [AlignRight, AlignLeft]) rows
  
  flalign :: TP Exp
  flalign = do
    rows <- sepEndBy1 arrayLine endLineAMS
    let n = maximum $ map length rows
    return $ EArray (take n $ cycle [AlignLeft, AlignRight]) rows
  
  cases :: TP Exp
  cases = do
    rs <- sepEndBy1 arrayLine endLineAMS
    return $ EDelimited "{" "" [Right $ EArray (alignsFromRows AlignLeft rs) rs]
  
  variable :: TP Exp
  variable = do
    v <- letter
    spaces
    return $ EIdentifier $ T.singleton v
  
  isConvertible :: Exp -> Bool
  isConvertible (EMathOperator x) = x `elem` convertibleOps
    where convertibleOps = [ "lim","liminf","limsup","inf","sup"
                           , "min","max","Pr","det","gcd" ]
  isConvertible (ESymbol Op x) = x `elem` convertibleSyms
    where convertibleSyms =
           ["\x2211", "\x220F", "\x2210", -- \sum \prod \amalg
            "\x22C0", "\x22C1", -- bigwedge \bigvee
            "\x22C2", "\x22C3", -- \bigcap \bigcup
            "\x2A05", "\x2A06"] -- \bigsqcap \bisqcup
  isConvertible _ = False
  
  -- check if sub/superscripts should always be under and over the expression
  isUnderover :: Exp -> Bool
  isUnderover (EOver _ _ (ESymbol TOver "\xFE37")) = True   -- \overbrace
  isUnderover (EOver _ _ (ESymbol TOver "\x23B4")) = True   -- \overbracket
  isUnderover (EOver _  _ (ESymbol TOver "\x23DE")) = True  -- \overbrace
  isUnderover (EUnder _ _ (ESymbol TUnder "\xFE38")) = True  -- \underbrace
  isUnderover (EUnder _ _ (ESymbol TUnder "\x23B5")) = True  -- \underbracket
  isUnderover (EUnder _  _ (ESymbol TUnder "\x23DF")) = True  -- \underbrace
  isUnderover _ = False
  
  subSup :: Maybe Bool -> Bool -> Exp -> TP Exp
  subSup limits convertible a = try $ do
    let sub1 = symbol "_" >> expr1  -- see #273
    let sup1 = (symbol "^" >> expr1) <|> primes
    (b,c) <- try (do {m <- sub1; n <- sup1; return (m,n)})
         <|> (do {n <- sup1; m <- sub1; return (m,n)})
    return $ case limits of
              Just True  -> EUnderover False a b c
              Nothing | convertible || isConvertible a -> EUnderover True a b c
                      | isUnderover a -> EUnderover False a b c
              _          -> ESubsup a b c
  
  superOrSubscripted :: Maybe Bool -> Bool -> Exp -> TP Exp
  superOrSubscripted limits convertible a = try $ do
    (c,b) <- ((,) <$> oneOf "^_" <*> (spaces >> expr))
            <|> (('^',) <$> primes)
    case c of
         '^' -> return $ case limits of
                          Just True  -> EOver False a b
                          Nothing
                            | convertible || isConvertible a -> EOver True a b
                            | isUnderover a -> EOver False a b
                          _          -> ESuper a b
         '_' -> return $ case limits of
                          Just True  -> EUnder False a b
                          Nothing
                            | convertible || isConvertible a -> EUnder True a b
                            | isUnderover a -> EUnder False a b
                          _          -> ESub a b
         _   -> mzero
  
  unicode :: TP Exp
  unicode = lexeme $
    do
      c <- satisfy (not . isAscii)
      return (ESymbol (getSymbolType c) $ T.singleton c)
  
  ensuremath :: Text -> TP Exp
  ensuremath "\\ensuremath" = inbraces
  ensuremath _ = mzero
  
  phantom :: Text -> TP Exp
  phantom "\\phantom" = EPhantom <$> texToken
  phantom _ = mzero
  
  boxed :: Text -> TP Exp
  boxed "\\boxed" = EBoxed <$> texToken
  boxed _ = mzero
  
  cancel :: Text -> TP Exp
  cancel c = do
    case c of
      "\\cancel"  -> ECancel ForwardSlash <$> texToken
      "\\bcancel" -> ECancel BackSlash <$> texToken
      "\\xcancel" -> ECancel XSlash <$> texToken
      _           -> mzero
  
  text :: Text -> TP Exp
  text c = do
    op <- maybe mzero return $ M.lookup c textOps
    char '{'
    let chunk = ((op . T.concat) <$> many1 textual)
              <|> (char '{' *> (asGroup <$> manyTill chunk (char '}')))
              <|> innermath
    contents <- manyTill chunk (char '}')
    spaces
    case contents of
         []   -> return (op "")
         [x]  -> return x
         xs   -> return (EGrouped xs)
  
  innermath :: TP Exp
  innermath = choice $ map innerMathWith
                [("$","$"),("$$","$$"),("\\(","\\)"),("\\[","\\]")]
  
  innerMathWith :: (String, String) -> TP Exp
  innerMathWith (opener, closer) = do
    try (string opener)
    e <- manyExp expr
    string closer
    return e
  
  styled :: Text -> TP Exp
  styled c = do
    case M.lookup c styleOps of
         Just f   -> do
           x <- texSymbol <|> inbraces <|> texChar
           return $ case x of
                         EGrouped xs -> f xs
                         _           -> f [x]
         Nothing  -> mzero
  
  colored :: Text -> TP Exp
  colored "\\color" = do
    _ <- inbraces -- skip the color
    -- in the future we might add color to the types or to the styles
    texSymbol <|> inbraces <|> texChar
  colored _ = mzero
  
  -- note: sqrt can be unary, \sqrt{2}, or binary, \sqrt[3]{2}
  root :: Text -> TP Exp
  root c = do
    guard $ c == "\\sqrt" || c == "\\surd"
    (ERoot <$> inbrackets <*> texToken) <|> (ESqrt <$> texToken)
  
  xspace :: Text -> TP Exp
  xspace "\\enspace" = return $ ESpace (1/2)
  xspace "\\mspace" =
    braces $ do
      len <- many1 digit
      lexeme $ string "mu"
      case reads len of
         ((n :: Integer,[]):_) -> return $ ESpace (fromIntegral n/18)
         _                     -> mzero
  xspace "\\hspace" = do
    braces $ do
      neg <- option 1 $ (-1) <$ char '-'
      as <- option "" $ many1 digit
      bs <- option "" $ char '.' *> many1 digit
      let denominator = 10^(length bs)
      as' <- if null as then pure 0 else stringToInteger as
      bs' <- if null bs then pure 0 else stringToInteger bs
      let numerator = (as' * denominator) + bs'
      let n = neg * (numerator % denominator)
      scaleFactor <- lexeme $
             1      <$ (string "em")
        <|> (1/12)  <$ (string "pt")
        <|> 6       <$ (string "in")
        <|> (50/21) <$ (string "cm")
      return $ ESpace (n * scaleFactor)
  xspace _ = mzero
  
  stringToInteger :: String -> TP Integer
  stringToInteger s =
    case reads s of
      ((n :: Integer, []):_) -> pure n
      _ -> fail $ "Could not read " <> s <> " as Integer."
  
  mathop :: Text -> TP Exp
  mathop c =
    case c of
      "\\mathop"    -> mathopWith Op
      "\\mathrel"   -> mathopWith Rel
      "\\mathbin"   -> mathopWith Bin
      "\\mathord"   -> mathopWith Ord
      "\\mathopen"  -> mathopWith Open
      "\\mathclose" -> mathopWith Close
      "\\mathpunct" -> mathopWith Pun
      _              -> mzero
  
  mathopWith :: TeXSymbolType -> TP Exp
  mathopWith ty = do
    e <- inbraces <|> expr1
    let es' = case e of
                   EGrouped xs -> xs
                   x           -> [x]
    case es' of
       [ESymbol _ x]   -> return $ ESymbol ty x
       [EIdentifier x] -> return $ ESymbol ty x
       [EText TextNormal x] -> return $ ESymbol ty x
       [EText sty x] -> return $ EStyled sty [ESymbol ty x]
       xs | ty == Op  -> return $ EMathOperator $
                           T.concat $ mapMaybe expToOperatorName xs
          | otherwise -> return $ EGrouped xs
  
  binary :: Text -> TP Exp
  binary c = do
    case c of
       "\\overset"  -> do
         a <- texToken
         b <- texToken
         return $ EOver False b a
       "\\stackrel" -> do
         a <- texToken
         b <- texToken
         return $ EOver False b a
       "\\underset" -> do
         a <- texToken
         b <- texToken
         return $ EUnder False b a
       "\\frac"     -> EFraction NormalFrac <$> texToken <*> texToken
       "\\tfrac"    -> EFraction InlineFrac <$> texToken <*> texToken
       "\\dfrac"    -> EFraction DisplayFrac <$> texToken <*> texToken
       "\\binom"    -> do
         a <- texToken
         b <- texToken
         return $ EDelimited "(" ")" [Right (EFraction NoLineFrac a b)]
       _            -> mzero
  
  texSymbol :: TP Exp
  texSymbol = operator <|>
              try (do c <- anyCtrlSeq
                      tSymbol c <|> negated c)
  
  negated :: Text -> TP Exp
  negated "\\not" = do
    sym <- texSymbol <|> texChar
    case sym of
      ESymbol Rel x -> return $ ESymbol Rel $ toNeg x
      EText tt x    -> return $ EText tt $ toNeg x
      ENumber x     -> return $ ENumber $ toNeg x
      EIdentifier x -> return $ EIdentifier $ toNeg x
      _             -> mzero
  negated _ = mzero
  
  toNeg :: Text -> Text
  toNeg x = case x of
              "\x2203" -> "\x2204"
              "\x2208" -> "\x2209"
              "\x220B" -> "\x220C"
              "\x2223" -> "\x2224"
              "\x2225" -> "\x2226"
              "\x2243" -> "\x2244"
              "\x2245" -> "\x2246"
              "\x2248" -> "\x2249"
              "="      -> "\x2260"
              "\x2261" -> "\x2262"
              "<"      -> "\x226E"
              ">"      -> "\x226F"
              "\x2264" -> "\x2270"
              "\x2265" -> "\x2271"
              "\x2272" -> "\x2274"
              "\x2273" -> "\x2275"
              "\x227A" -> "\x2280"
              "\x227B" -> "\x2281"
              "\x2282" -> "\x2284"
              "\x2283" -> "\x2285"
              "\x2286" -> "\x2288"
              "\x2287" -> "\x2289"
              "\x227C" -> "\x22E0"
              "\x227D" -> "\x22E1"
              "\x2291" -> "\x22E2"
              "\x2292" -> "\x22E3"
              _        -> x <> "\x0338"
  
  
  oneOfCommands :: [Text] -> TP Text
  oneOfCommands cmds = try $ do
    cmd <- oneOfStrings cmds
    case T.unpack cmd of
      ['\\',c] | not (isLetter c) -> return ()
      cmd' -> (do pos <- getPosition
                  letter
                  setPosition pos
                  mzero <?> ("non-letter after " <> cmd'))
           <|> return ()
    spaces
    return cmd
  
  oneOfStrings' :: (Char -> Char -> Bool) -> [(String, Text)] -> TP Text
  oneOfStrings' _ [] = mzero
  oneOfStrings' matches strs = try $ do
      c <- anyChar
      let strs' = [(xs, t) | ((x:xs), t) <- strs, x `matches` c]
      case strs' of
        []  -> mzero
        _   -> oneOfStrings' matches strs'
               <|> case find (null . fst) strs' of
                     Just (_, t) -> return t
                     Nothing     -> mzero
  
  -- | Parses one of a list of strings.  If the list contains
  -- two strings one of which is a prefix of the other, the longer
  -- string will be matched if possible.
  oneOfStrings :: [Text] -> TP Text
  oneOfStrings strs = oneOfStrings' (==) strs' <??> (intercalate ", " $ map show strs)
    where
      strs' = map (\x -> (T.unpack x, x)) strs
  
  -- | Like '(<?>)', but moves position back to the beginning of the parse
  -- before reporting the error.
  (<??>) :: Monad m => ParsecT s u m a -> String -> ParsecT s u m a
  (<??>) p expected = do
    pos <- getPosition
    p <|> (setPosition pos >> mzero <?> expected)
  
  infix 0 <??>
  
  tSymbol :: Text -> TP Exp
  tSymbol sym =
    case M.lookup sym symbols of
         Just acc@(ESymbol Accent _) ->
           (\t -> EOver False t acc) <$> texToken
         Just acc@(ESymbol TUnder _) ->
           (\t -> EUnder False t acc) <$> texToken
         Just acc@(ESymbol TOver _) ->
           (\t -> EOver False t acc) <$> texToken
         Just (ESymbol Alpha t) -> return $ EIdentifier t -- see #256
         Just x  -> return x
         Nothing
           | sym == "\\mod" -> do
               x <- deGroup <$> expr
               return $ EGrouped
                 [ESpace (8/18), EMathOperator "mod", ESpace (4/18), x]
           | sym == "\\bmod" -> do
               x <- deGroup <$> expr
               return $ EGrouped
                 [ESpace (4/18), EMathOperator "mod", ESpace (4/18), x]
           | sym == "\\pmod" -> do
               x <- deGroup <$> expr
               return $ EGrouped
                 [ESpace (4/18), ESymbol Open "(", EMathOperator "mod",
                  ESpace (4/18), x, ESymbol Close ")"]
           | sym == "\\pod"  -> do
               x <- deGroup <$> expr
               return $ EGrouped
                 [ESpace (4/18), ESymbol Open "(", x, ESymbol Close ")"]
           | otherwise -> mzero
  
  operator :: TP Exp
  operator = do
    sym <- lexeme (oneOfStrings $ M.keys operators)
    return $ fromJust (M.lookup sym operators)
  
  lexeme :: TP a -> TP a
  lexeme p = p <* ignorable
  
  braces :: TP a -> TP a
  braces p = lexeme $ char '{' *> spaces *> p <* spaces <* char '}'
  
  brackets :: TP a -> TP a
  brackets p = lexeme $ char '[' *> spaces *> p <* spaces <* char ']'
  
  textStr :: Text -> TP Text
  textStr t = string (T.unpack t) $> t
  
  countChar :: Int -> TP Char -> TP Text
  countChar n = fmap T.pack . count n
  
  symbol :: String -> TP String
  symbol s = lexeme $ try $ string s
  
  -- text mode parsing
  
  textual :: TP Text
  textual = regular <|> sps <|> ligature <|> textCommand
              <?> "text"
  
  sps :: TP Text
  sps = " " <$ skipMany1 (oneOf " \t\n")
  
  regular :: TP Text
  regular = T.pack <$> many1 (noneOf "`'-~${}\\ \t")
  
  ligature :: TP Text
  ligature = try ("\x2014" <$ string "---")
         <|> try ("\x2013" <$ string "--")
         <|> try (textStr "-")
         <|> try ("\x201C" <$ string "``")
         <|> try ("\x201D" <$ string "''")
         <|> try ("\x2019" <$ string "'")
         <|> try ("\x2018" <$ string "`")
         <|> try ("\xA0"   <$ string "~")
  
  textCommand :: TP Text
  textCommand = do
    cmd <- oneOfCommands (M.keys textCommands)
    optional $ try (char '{' >> spaces >> char '}')
    case M.lookup cmd textCommands of
         Nothing -> fail $ T.unpack $ "Unknown control sequence " <> cmd
         Just c  -> c
  
  tok :: TP Char
  tok = (try $ char '{' *> spaces *> anyChar <* spaces <* char '}')
     <|> anyChar
  
  textCommands :: M.Map Text (TP Text)
  textCommands = M.fromList
    [ ("\\#", return "#")
    , ("\\$", return "$")
    , ("\\%", return "%")
    , ("\\&", return "&")
    , ("\\_", return "_")
    , ("\\{", return "{")
    , ("\\}", return "}")
    , ("\\ldots", return "\x2026")
    , ("\\textasciitilde", return "~")
    , ("\\textasciicircum", return "^")
    , ("\\textbackslash", return "\\")
    , ("\\char", parseC)
    , ("\\aa", return "å")
    , ("\\AA", return "Å")
    , ("\\ss", return "ß")
    , ("\\o", return "ø")
    , ("\\O", return "Ø")
    , ("\\L", return "Ł")
    , ("\\l", return "ł")
    , ("\\ae", return "æ")
    , ("\\AE", return "Æ")
    , ("\\oe", return "œ")
    , ("\\OE", return "Œ")
    , ("\\`", option "`" $ grave <$> tok)
    , ("\\'", option "'" $ acute <$> tok)
    , ("\\^", option "^" $ circ  <$> tok)
    , ("\\~", option "~" $ tilde <$> tok)
    , ("\\\"", option "\"" $ try $ umlaut <$> tok)
    , ("\\.", option "." $ try $ dot <$> tok)
    , ("\\=", option "=" $ try $ macron <$> tok)
    , ("\\c", option "c" $ try $ cedilla <$> tok)
    , ("\\v", option "v" $ try $ hacek <$> tok)
    , ("\\u", option "u" $ try $ breve <$> tok)
    , ("\\ ", return " ")
    , ("\\quad", return "\x2000")
    ]
  
  parseC :: TP Text
  parseC = try $ char '`' >> countChar 1 anyChar
  
  -- the functions below taken from pandoc:
  
  grave :: Char -> Text
  grave 'A' = "À"
  grave 'E' = "È"
  grave 'I' = "Ì"
  grave 'O' = "Ò"
  grave 'U' = "Ù"
  grave 'a' = "à"
  grave 'e' = "è"
  grave 'i' = "ì"
  grave 'o' = "ò"
  grave 'u' = "ù"
  grave c   = T.singleton c
  
  acute :: Char -> Text
  acute 'A' = "Á"
  acute 'E' = "É"
  acute 'I' = "Í"
  acute 'O' = "Ó"
  acute 'U' = "Ú"
  acute 'Y' = "Ý"
  acute 'a' = "á"
  acute 'e' = "é"
  acute 'i' = "í"
  acute 'o' = "ó"
  acute 'u' = "ú"
  acute 'y' = "ý"
  acute 'C' = "Ć"
  acute 'c' = "ć"
  acute 'L' = "Ĺ"
  acute 'l' = "ĺ"
  acute 'N' = "Ń"
  acute 'n' = "ń"
  acute 'R' = "Ŕ"
  acute 'r' = "ŕ"
  acute 'S' = "Ś"
  acute 's' = "ś"
  acute 'Z' = "Ź"
  acute 'z' = "ź"
  acute c   = T.singleton c
  
  circ :: Char -> Text
  circ 'A' = "Â"
  circ 'E' = "Ê"
  circ 'I' = "Î"
  circ 'O' = "Ô"
  circ 'U' = "Û"
  circ 'a' = "â"
  circ 'e' = "ê"
  circ 'i' = "î"
  circ 'o' = "ô"
  circ 'u' = "û"
  circ 'C' = "Ĉ"
  circ 'c' = "ĉ"
  circ 'G' = "Ĝ"
  circ 'g' = "ĝ"
  circ 'H' = "Ĥ"
  circ 'h' = "ĥ"
  circ 'J' = "Ĵ"
  circ 'j' = "ĵ"
  circ 'S' = "Ŝ"
  circ 's' = "ŝ"
  circ 'W' = "Ŵ"
  circ 'w' = "ŵ"
  circ 'Y' = "Ŷ"
  circ 'y' = "ŷ"
  circ c   = T.singleton c
  
  tilde :: Char -> Text
  tilde 'A' = "Ã"
  tilde 'a' = "ã"
  tilde 'O' = "Õ"
  tilde 'o' = "õ"
  tilde 'I' = "Ĩ"
  tilde 'i' = "ĩ"
  tilde 'U' = "Ũ"
  tilde 'u' = "ũ"
  tilde 'N' = "Ñ"
  tilde 'n' = "ñ"
  tilde c   = T.singleton c
  
  umlaut :: Char -> Text
  umlaut 'A' = "Ä"
  umlaut 'E' = "Ë"
  umlaut 'I' = "Ï"
  umlaut 'O' = "Ö"
  umlaut 'U' = "Ü"
  umlaut 'a' = "ä"
  umlaut 'e' = "ë"
  umlaut 'i' = "ï"
  umlaut 'o' = "ö"
  umlaut 'u' = "ü"
  umlaut c   = T.singleton c
  
  dot :: Char -> Text
  dot 'C' = "Ċ"
  dot 'c' = "ċ"
  dot 'E' = "Ė"
  dot 'e' = "ė"
  dot 'G' = "Ġ"
  dot 'g' = "ġ"
  dot 'I' = "İ"
  dot 'Z' = "Ż"
  dot 'z' = "ż"
  dot c   = T.singleton c
  
  macron :: Char -> Text
  macron 'A' = "Ā"
  macron 'E' = "Ē"
  macron 'I' = "Ī"
  macron 'O' = "Ō"
  macron 'U' = "Ū"
  macron 'a' = "ā"
  macron 'e' = "ē"
  macron 'i' = "ī"
  macron 'o' = "ō"
  macron 'u' = "ū"
  macron c   = T.singleton c
  
  cedilla :: Char -> Text
  cedilla 'c' = "ç"
  cedilla 'C' = "Ç"
  cedilla 's' = "ş"
  cedilla 'S' = "Ş"
  cedilla 't' = "ţ"
  cedilla 'T' = "Ţ"
  cedilla 'e' = "ȩ"
  cedilla 'E' = "Ȩ"
  cedilla 'h' = "ḩ"
  cedilla 'H' = "Ḩ"
  cedilla 'o' = "o̧"
  cedilla 'O' = "O̧"
  cedilla c   = T.singleton c
  
  hacek :: Char -> Text
  hacek 'A' = "Ǎ"
  hacek 'a' = "ǎ"
  hacek 'C' = "Č"
  hacek 'c' = "č"
  hacek 'D' = "Ď"
  hacek 'd' = "ď"
  hacek 'E' = "Ě"
  hacek 'e' = "ě"
  hacek 'G' = "Ǧ"
  hacek 'g' = "ǧ"
  hacek 'H' = "Ȟ"
  hacek 'h' = "ȟ"
  hacek 'I' = "Ǐ"
  hacek 'i' = "ǐ"
  hacek 'j' = "ǰ"
  hacek 'K' = "Ǩ"
  hacek 'k' = "ǩ"
  hacek 'L' = "Ľ"
  hacek 'l' = "ľ"
  hacek 'N' = "Ň"
  hacek 'n' = "ň"
  hacek 'O' = "Ǒ"
  hacek 'o' = "ǒ"
  hacek 'R' = "Ř"
  hacek 'r' = "ř"
  hacek 'S' = "Š"
  hacek 's' = "š"
  hacek 'T' = "Ť"
  hacek 't' = "ť"
  hacek 'U' = "Ǔ"
  hacek 'u' = "ǔ"
  hacek 'Z' = "Ž"
  hacek 'z' = "ž"
  hacek c   = T.singleton c
  
  breve :: Char -> Text
  breve 'A' = "Ă"
  breve 'a' = "ă"
  breve 'E' = "Ĕ"
  breve 'e' = "ĕ"
  breve 'G' = "Ğ"
  breve 'g' = "ğ"
  breve 'I' = "Ĭ"
  breve 'i' = "ĭ"
  breve 'O' = "Ŏ"
  breve 'o' = "ŏ"
  breve 'U' = "Ŭ"
  breve 'u' = "ŭ"
  breve c   = T.singleton c
  
  -- siunitx
  
  siunitx :: Text -> TP Exp
  siunitx c = do
    case c of
      "\\si"       -> dosi
      "\\unit"     -> dosi  -- v3 version of \si
      "\\SI"       -> doSI
      "\\qty"      -> doSI  -- v3 version of \SI
      "\\SIrange"  -> doSIrange True
      "\\qtyrange" -> doSIrange True -- v3 version of SIrange
      "\\numrange" -> doSIrange False
      "\\numlist"  -> doSInumlist
    -- "\\SIlist"   -> doSIlist -- v3 version of SIlist
    -- "\\qtylist"  -> doSIlist -- v3 version of SIlist
      "\\num"      -> doSInum
      "\\ang"      -> doSIang
      _          -> mzero
  
  -- converts e.g. \SIrange{100}{200}{\ms} to "100 ms--200 ms"
  doSIrange :: Bool -> TP Exp
  doSIrange includeUnits = do
    optional $ skipMany inbrackets
    startvalue <- Just <$> doSInum
    startvalueprefix <- option Nothing $ Just <$> brackets expr
    stopvalue <- Just <$> doSInum
    stopvalueprefix <- option Nothing $ Just <$> brackets expr
    unit <- if includeUnits
               then option Nothing $ Just <$> dosi
               else return Nothing
    return $ EGrouped $ catMaybes
             [startvalueprefix,
              emptyOr160 startvalueprefix,
              startvalue,
              emptyOr160 unit,
              unit,
              Just (EText TextNormal "\8211"), -- An en-dash
              stopvalueprefix,
              emptyOr160 stopvalueprefix,
              stopvalue,
              emptyOr160 unit,
              unit]
  
  
  doSInumlist :: TP Exp
  doSInumlist = do
    optional $ skipMany inbrackets
    xs <- braces (sepBy siNum (spaces *> char ';' <* spaces))
    return $
      case xs of
        []  -> EGrouped []
        [x] -> x
        _   -> EGrouped $
                 intersperse (EText TextNormal ", ") (init xs) ++
                 [EText TextNormal ", & ", last xs]
  
  dosi :: TP Exp
  dosi = siUnit <|> braces (manyExp (siUnit <|> expr))
  
  doSIang :: TP Exp
  doSIang = do
    optional $ skipMany inbrackets
    ps <- braces $ sepBy siNum (spaces *> char ';' <* spaces)
    return $ EGrouped $
      case ps ++ repeat (EGrouped []) of
        (d:m:s:_) ->
          (if d == EGrouped [] then [] else [d, EText TextNormal "\xb0"]) <>
          (if m == EGrouped [] then [] else [m, EText TextNormal "\x2032"]) <>
          (if s == EGrouped [] then [] else [s, EText TextNormal "\x2033"])
        _ -> []
  
  -- converts e.g. \SI{1}[\$]{} to "$ 1" or \SI{1}{\euro} to "1 €"
  doSI :: TP Exp
  doSI = do
    optional $ skipMany inbrackets
    value <- Just <$> doSInum
    valueprefix <- option Nothing $ do
                      x <- inbrackets
                      if x == EGrouped []
                         then return Nothing
                         else return $ Just x
    unit <- option Nothing $ Just <$> dosi
    return $ EGrouped $ catMaybes
           [ valueprefix,
             emptyOr160 valueprefix,
             value,
             emptyOr160 unit,
             unit
           ]
  
  emptyOr160 :: Maybe Exp -> Maybe Exp
  emptyOr160 (Just _) = Just (ESpace (4/18))
  emptyOr160 Nothing  = Nothing
  
  
  siUnit :: TP Exp
  siUnit = try $ do
    name <- T.dropWhile (=='\\') <$> anyCtrlSeq
    case name of
      "square" -> do
         unit <- siUnit
         return $ ESuper unit (ENumber "2")
      "cubic" -> do
         unit <- siUnit
         return $ ESuper unit (ENumber "3")
      "raisetothe" -> do
         n <- expr
         unit <- siUnit
         return $ ESuper unit n
      _ ->
         case M.lookup name siUnitMap of
              Just il ->
                option il $
                  choice
                   [ (ESuper il (ENumber "2")) <$ ctrlseq "squared"
                   , (ESuper il (ENumber "3")) <$ ctrlseq "cubed"
                   , (\n -> ESuper il n) <$> (ctrlseq "tothe" *> expr)
                   ]
              Nothing -> fail "not an siunit unit command"
  
  doSInum :: TP Exp
  doSInum = do
    optional $ skipMany inbrackets
    braces siNum
  
  siNum :: TP Exp
  siNum = asGroup . mconcat <$> many parseNumPart
  
  parseNumPart :: TP [Exp]
  parseNumPart =
    parseDecimalNum <|>
    parseComma <|>
    parsePlusMinus <|>
    parseI <|>
    parseExp <|>
    parseX <|>
    parseSpace
   where
    parseDecimalNum = do
      pref <- option mempty $ (mempty <$ char '+') <|> ("\x2212" <$ char '-')
      basenum <- (pref <>) . T.pack
                  <$> many1 (satisfy (\c -> isDigit c || c == '.'))
      uncertainty <- option mempty $ T.pack <$> parseParens
      if T.null uncertainty
         then return [ENumber basenum]
         else return [ENumber $ basenum <> "\xa0\xb1\xa0" <>
               let (_,ys) = T.break (=='.') basenum
                in case (T.length ys - 1, T.length uncertainty) of
                     (0,_) -> uncertainty
                     (x,y)
                       | x > y  -> "0." <> T.replicate (x - y) "0" <>
                                        T.dropWhileEnd (=='0') uncertainty
                       | otherwise -> T.take (y - x) uncertainty <>
                                        case T.dropWhileEnd (=='0')
                                               (T.drop (y - x) uncertainty) of
                                               t | T.null t -> mempty
                                                 | otherwise -> "." <> t]
    parseComma = [ENumber "."] <$ char ','
    parsePlusMinus = [EText TextNormal "\xa0\xb1\xa0"] <$ try (string "+-")
    parseParens =
      char '(' *> many1 (satisfy (\c -> isDigit c || c == '.')) <* char ')'
    parseI = [EIdentifier "i"] <$ char 'i'
    parseX = [ESymbol Rel "\xa0\xd7\xa0"] <$ char 'x'
    parseExp = do
      n <- asGroup <$> (char 'e' *> parseDecimalNum)
      return $ [ESymbol Rel "\xa0\xd7\xa0", ESuper (ENumber "10") n ]
    parseSpace = mempty <$ skipMany1 (char ' ')
  
  arrow :: Text -> TP Exp
  arrow c = case c of
    "\\xleftarrow"   -> underoverarrow "\x2190"
    "\\xrightarrow"  -> underoverarrow "\x2192"
    _                -> mzero
   where
    underoverarrow s = do
      munder <- optionMaybe inbrackets
      over <- texToken
      return $ case munder of
        Nothing    -> EOver False (ESymbol Op s) over
        Just under -> EUnderover False (ESymbol Op s) under over
  
  