diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12857862..da837edc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,6 @@ jobs: - name: Install dependencies run: | cabal v2-update - cabal freeze --dependencies-only --enable-tests --disable-optimization -fexecutable - cat cabal.project.freeze cabal v2-build --dependencies-only --enable-tests --disable-optimization -fexecutable all - name: Build and test run: | diff --git a/README.md b/README.md index 60724382..caadeedc 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ tests](https://github.com/jgm/texmath/workflows/CI%20tests/badge.svg)](https://g texmath is a Haskell library for converting between formats used to represent mathematics. Currently it provides functions to read and write TeX math, presentation MathML, and OMML (Office Math Markup -Language, used in Microsoft Office), and to write Gnu eqn, typst, and -[pandoc]'s native format (allowing conversion, using pandoc, to -a variety of different markup formats). The TeX reader and -writer supports basic LaTeX and AMS extensions, and it can parse -and apply LaTeX macros. The package also includes several -utility modules which may be useful for anyone looking to -manipulate either TeX math or MathML. For example, a copy of -the MathML operator dictionary is included. +Language, used in Microsoft Office), and to write Gnu eqn, typst, +StarMath (used by LibreOffice), and [pandoc]'s native format +(allowing conversion, using pandoc, to a variety of different +markup formats). The TeX reader and writer supports basic LaTeX +and AMS extensions, and it can parse and apply LaTeX macros. The +package also includes several utility modules which may be useful +for anyone looking to manipulate either TeX math or MathML. For +example, a copy of the MathML operator dictionary is included. [pandoc]: http://github.com/jgm/pandoc diff --git a/extra/texmath.hs b/extra/texmath.hs index 03b2b15b..490eee96 100644 --- a/extra/texmath.hs +++ b/extra/texmath.hs @@ -62,6 +62,7 @@ writers = [ , ("omml", XMLWriter writeOMML) , ("xhtml", XMLWriter (\dt e -> inHtml (writeMathML dt e))) , ("mathml", XMLWriter writeMathML) + , ("starmath", StringWriter writeStarMath) , ("pandoc", PandocWriter writePandoc)] data Options = Options { diff --git a/man/texmath.1.md b/man/texmath.1.md index 8506d826..ab49242e 100644 --- a/man/texmath.1.md +++ b/man/texmath.1.md @@ -33,8 +33,8 @@ expect query parameters for `from`, `to`, `input`, and optionally Defaults to `tex`. `-t` *FORMAT* -: Specify output ("to") format: `tex`, `omml`, - `xhtml`, `mathml`, `pandoc`, `native`. Defaults to `mathml`. +: Specify output ("to") format: `tex`, `omml`, `xhtml`, `mathml`, + `typst`, `eqn`, `starmath`, `pandoc`, `native`. Defaults to `mathml`. `--inline` : Use the inline display style. diff --git a/server/Main.hs b/server/Main.hs index 855fac8b..f1a8817f 100644 --- a/server/Main.hs +++ b/server/Main.hs @@ -35,7 +35,7 @@ data Params = Params } deriving (Show) data Format = - TeX | MathML | Eqn | OMML | Typst + TeX | MathML | Eqn | OMML | Typst | StarMath deriving (Show, Ord, Eq) instance FromJSON Format where @@ -46,6 +46,7 @@ instance FromJSON Format where "eqn" -> pure Eqn "typst" -> pure Typst "omml" -> pure OMML + "starmath" -> pure StarMath _ -> fail $ "Unknown format " <> T.unpack s parseJSON _ = fail "Expecting string format" @@ -60,6 +61,7 @@ instance FromHttpApiData Format where "eqn" -> pure Eqn "typst" -> pure Typst "omml" -> pure OMML + "starmath" -> pure StarMath _ -> Left $ "Unknown format " <> t -- Automatically derive code to convert to/from JSON. @@ -132,12 +134,14 @@ server = convert MathML -> readMathML Eqn -> \_ -> Left "eqn reader not implemented" Typst -> \_ -> Left "typst reader not implemented" + StarMath -> \_ -> Left "StarMath reader not implemented" writer = case to params of Eqn -> writeEqn dt Typst -> writeTypst dt OMML -> T.pack . ppElement . writeOMML dt TeX -> writeTeX MathML -> T.pack . ppElement . writeMathML dt + StarMath -> writeStarMath dt in handleErr $ writer <$> reader txt handleErr (Right t) = return t diff --git a/src/Text/TeXMath.hs b/src/Text/TeXMath.hs index e3afc0d4..6e9b6c8c 100644 --- a/src/Text/TeXMath.hs +++ b/src/Text/TeXMath.hs @@ -62,6 +62,7 @@ module Text.TeXMath ( readMathML, writeTeXWith, addLaTeXEnvironment, writeEqn, + writeStarMath, writeTypst, writeOMML, writeMathML, @@ -78,5 +79,6 @@ import Text.TeXMath.Writers.OMML import Text.TeXMath.Writers.Pandoc import Text.TeXMath.Writers.TeX import Text.TeXMath.Writers.Eqn +import Text.TeXMath.Writers.StarMath import Text.TeXMath.Writers.Typst import Text.TeXMath.Types diff --git a/src/Text/TeXMath/Writers/StarMath.hs b/src/Text/TeXMath/Writers/StarMath.hs new file mode 100644 index 00000000..d6b55577 --- /dev/null +++ b/src/Text/TeXMath/Writers/StarMath.hs @@ -0,0 +1,931 @@ +{-# LANGUAGE OverloadedStrings #-} +module Text.TeXMath.Writers.StarMath + ( writeStarMath + ) where + +import qualified Data.List as List +import qualified Data.Text as T +import Text.TeXMath.Types + ( Alignment(..) + , DisplayType + , Exp(..) + , FractionType(..) + , TeXSymbolType(..) + , TextType(..) + ) +import Text.TeXMath.Writers.TeX (writeTeX) + +-- | Render TeXMath expressions as StarMath syntax. +-- Falls back to TeX output for expressions that are not yet supported. +writeStarMath :: DisplayType -> [Exp] -> T.Text +writeStarMath _dt exps = + case renderExps (normalizeExps exps) of + Just rendered -> T.strip rendered + Nothing -> writeTeX exps + +data AlignContext = AlignDefault | AlignLeftCtx | AlignRightCtx + deriving (Eq) + +renderExps :: [Exp] -> Maybe T.Text +renderExps = renderExpsIn AlignDefault + +normalizeExps :: [Exp] -> [Exp] +normalizeExps = normalizeBareBars . map normalizeExp + +normalizeExp :: Exp -> Exp +normalizeExp e = + case e of + EGrouped xs -> EGrouped (normalizeExps xs) + EStyled sty xs -> EStyled sty (normalizeExps xs) + EFraction ft num den -> EFraction ft (normalizeExp num) (normalizeExp den) + ESqrt x -> ESqrt (normalizeExp x) + ERoot idx rad -> ERoot (normalizeExp idx) (normalizeExp rad) + EDelimited op cl xs -> EDelimited op cl (map normalizeDelimitedPiece xs) + ESub base sub -> ESub (normalizeExp base) (normalizeExp sub) + ESuper base sup -> ESuper (normalizeExp base) (normalizeExp sup) + ESubsup base sub sup -> ESubsup (normalizeExp base) (normalizeExp sub) (normalizeExp sup) + EOver b base over -> EOver b (normalizeExp base) (normalizeExp over) + EUnder b base under -> EUnder b (normalizeExp base) (normalizeExp under) + EUnderover b base u o -> EUnderover b (normalizeExp base) (normalizeExp u) (normalizeExp o) + EArray aligns rows -> EArray aligns (map (map normalizeExps) rows) + EPhantom x -> EPhantom (normalizeExp x) + _ -> e + +normalizeDelimitedPiece :: Either T.Text Exp -> Either T.Text Exp +normalizeDelimitedPiece p = + case p of + Left t -> Left t + Right e -> Right (normalizeExp e) + +normalizeBareBars :: [Exp] -> [Exp] +normalizeBareBars [] = [] +normalizeBareBars (x : xs) + | Just sym <- bareBarSymbol x = + case break (matchesBareBar sym) xs of + (mid, y : rest) | matchesBareBar sym y -> + EDelimited sym sym (map Right mid) : normalizeBareBars rest + _ -> + x : normalizeBareBars xs +normalizeBareBars (x:xs) = x : normalizeBareBars xs + +bareBarSymbol :: Exp -> Maybe T.Text +bareBarSymbol e = + case e of + ESymbol _ "|" -> Just "|" + ESymbol _ "∣" -> Just "|" + ESymbol _ "∥" -> Just "∥" + _ -> Nothing + +matchesBareBar :: T.Text -> Exp -> Bool +matchesBareBar sym e = + case bareBarSymbol e of + Just sym' -> sym == sym' + Nothing -> False + +renderExpsIn :: AlignContext -> [Exp] -> Maybe T.Text +renderExpsIn ctx exps = do + rendered <- mapM (renderExpIn ctx) exps + let pieces = zip exps rendered + let merged = mergePieces pieces + let withLhs = + if startsWithInfixNeedingLhs exps + then "{} " <> T.stripStart merged + else merged + pure $ if endsWithInfixNeedingRhs exps + then T.stripEnd withLhs <> " {}" + else withLhs + +mergePieces :: [(Exp, T.Text)] -> T.Text +mergePieces [] = "" +mergePieces ((e0, t0) : rest) = snd $ List.foldl' step (e0, t0) rest + where + step (prevE, acc) (curE, curT) = + if T.null curT + then (prevE, acc) + else + let sep = if needsSeparator prevE curE then " " else "" + in (curE, acc <> sep <> curT) + +needsSeparator :: Exp -> Exp -> Bool +needsSeparator prevE curE + | isGreekIdentifierExp prevE && isIdentifierLike curE = True + | isGreekIdentifierExp prevE && isTerminatingPunctuation curE = True + | isWordSymbolLike prevE && isIdentifierLike curE = True + | isWordStyledExp prevE && isIdentifierLike curE = True + | isMathOperatorExp prevE && isIdentifierLike curE = True + | isUnaryMinusSymbol prevE && isIdentifierLike curE = True + | isIdentifierLike prevE && isMathOperatorExp curE = True + | isScripted prevE && not (isLargeOpScripted prevE) && + isIdentifierLike curE = True + | isCloseLike prevE && isIdentifierLike curE = True + | isIdentifierLike prevE && isWordSymbolLike curE = True + | isIdentifierLike prevE && isWordStyledExp curE = True + | isIdentifierLike prevE && isWideSpace curE = True + | isIdentifierLike prevE && isDelimited curE = True + | otherwise = False + +isGreekIdentifierExp :: Exp -> Bool +isGreekIdentifierExp e = + case e of + EIdentifier t -> greekName t /= Nothing + _ -> False + +isIdentifierLike :: Exp -> Bool +isIdentifierLike e = + case e of + EIdentifier{} -> True + ENumber{} -> True + EMathOperator{} -> True + ESub{} -> True + ESuper{} -> True + ESubsup{} -> True + EStyled{} -> True + _ -> False + +isMathOperatorExp :: Exp -> Bool +isMathOperatorExp e = + case e of + EMathOperator{} -> True + _ -> False + +isDelimited :: Exp -> Bool +isDelimited e = + case e of + EDelimited{} -> True + _ -> False + +isWideSpace :: Exp -> Bool +isWideSpace e = + case e of + ESpace w -> w >= 1 + _ -> False + +isWordSymbolLike :: Exp -> Bool +isWordSymbolLike e = + case e of + ESymbol _ "∀" -> True + ESymbol _ "∃" -> True + ESymbol _ "∇" -> True + _ -> False + +isWordStyledExp :: Exp -> Bool +isWordStyledExp e = + case e of + EStyled TextNormal [x] -> isWordStyledExp x + EStyled TextItalic _ -> True + EStyled TextBold _ -> True + EStyled TextScript _ -> True + EStyled TextFraktur _ -> True + EStyled TextDoubleStruck _ -> True + _ -> False + +isTerminatingPunctuation :: Exp -> Bool +isTerminatingPunctuation e = + case e of + ESymbol _ "." -> True + ESymbol _ "," -> True + ESymbol _ ";" -> True + ESymbol _ ":" -> True + _ -> False + +isCloseLike :: Exp -> Bool +isCloseLike e = + case e of + ESymbol Close _ -> True + EDelimited{} -> True + _ -> False + +isScripted :: Exp -> Bool +isScripted e = + case e of + ESub{} -> True + ESuper{} -> True + ESubsup{} -> True + _ -> False + +isUnaryMinusSymbol :: Exp -> Bool +isUnaryMinusSymbol e = + case e of + ESymbol t "-" -> t /= Bin + ESymbol t "−" -> t /= Bin + _ -> False + +isLargeOpScripted :: Exp -> Bool +isLargeOpScripted e = + case e of + ESub base _ -> largeOpName base /= Nothing + ESuper base _ -> largeOpName base /= Nothing + ESubsup base _ _ -> largeOpName base /= Nothing + _ -> False + +startsWithInfixNeedingLhs :: [Exp] -> Bool +startsWithInfixNeedingLhs exps = + case exps of + (e : _) -> needsNeutralLhs e + _ -> False + +endsWithInfixNeedingRhs :: [Exp] -> Bool +endsWithInfixNeedingRhs exps = + case reverse exps of + (e : _) -> needsNeutralRhs e + _ -> False + +needsNeutralLhs :: Exp -> Bool +needsNeutralLhs = isInfixLikeExp + +needsNeutralRhs :: Exp -> Bool +needsNeutralRhs = isInfixLikeExp + +isInfixLikeExp :: Exp -> Bool +isInfixLikeExp e = + case e of + ESymbol t s + | t == Bin -> True + | t == Rel -> True + | otherwise -> s `elem` + [ "×", "⋅", "·", "∘" + , "∈", "∉", "∋" + , "∩", "∪" + , "⊂", "⊆", "⊃", "⊇" + , "≤", "≥", "≠", "≈", "≡", "∝" + , "∥", "⊥" + , "±", "∓" + , "/", "←", "→", "↔", "⇐", "⇒", "⇔", "↦" + ] + _ -> False + +renderExpIn :: AlignContext -> Exp -> Maybe T.Text +renderExpIn ctx e = + case e of + ENumber t -> Just t + EIdentifier t -> Just (renderIdentifier t) + EMathOperator t -> Just (renderMathOperator t) + ESymbol t s -> Just (renderSymbol t s) + EText _ t -> Just (quoteText t) + ESpace w -> Just (renderSpace w) + EGrouped xs -> ("{" <>) . (<> "}") <$> renderExpsIn ctx xs + EStyled sty xs -> renderStyled ctx sty xs + + EFraction frac num den -> do + num' <- renderExpIn AlignDefault num + den' <- renderExpIn AlignDefault den + let num'' = maybeCenterFractionArg ctx num' + let den'' = maybeCenterFractionArg ctx den' + pure $ case frac of + NoLineFrac -> "{" <> num'' <> " / " <> den'' <> "}" + _ -> "{" <> num'' <> " over " <> den'' <> "}" + + ESqrt x -> ("sqrt {" <>) . (<> "}") <$> renderExpIn ctx x + ERoot idx rad -> do + idx' <- renderExpIn ctx idx + rad' <- renderExpIn ctx rad + pure $ "nroot {" <> idx' <> "} {" <> rad' <> "}" + + EDelimited op cl xs -> do + body <- renderDelimitedBody ctx xs + let op' = delimToken DelimLeft op + let cl' = delimToken DelimRight cl + pure $ "left " <> op' <> " " <> body <> " right " <> cl' + + ESub base sub -> do + case limitOpName base of + Just op -> do + sub' <- renderLimitArg ctx sub + pure $ op <> " from " <> sub' <> " " + Nothing -> do + base' <- renderExpIn ctx base + sub' <- renderScriptArg ctx sub + pure $ renderScriptBase base base' <> "_" <> sub' + + ESuper base sup -> do + case limitOpName base of + Just op -> do + sup' <- renderLimitArg ctx sup + pure $ op <> " to " <> sup' <> " " + Nothing -> do + base' <- renderExpIn ctx base + sup' <- renderScriptArg ctx sup + pure $ renderScriptBase base base' <> "^" <> sup' + + ESubsup base sub sup -> do + case limitOpName base of + Just op -> do + sub' <- renderLimitArg ctx sub + sup' <- renderLimitArg ctx sup + pure $ op <> " from " <> sub' <> " to " <> sup' <> " " + Nothing -> do + base' <- renderExpIn ctx base + sub' <- renderScriptArg ctx sub + sup' <- renderScriptArg ctx sup + pure $ renderScriptBase base base' <> "_" <> sub' <> "^" <> sup' + + EOver _ base over + | Just accent <- accentName over -> do + base' <- renderExpIn ctx base + pure $ accent <> " " <> renderAccentArg base base' + | otherwise -> Nothing + + EUnder _ base under -> + case centeredScriptOpName base of + Just op -> do + under' <- renderScriptArg ctx under + pure $ "{" <> op <> "} csub " <> centerScriptArg under' + Nothing -> + case limitOpName base of + Just op -> do + under' <- renderLimitArg ctx under + pure $ op <> " from " <> under' <> " " + Nothing -> do + base' <- renderExpIn ctx base + under' <- renderScriptArg ctx under + pure $ renderScriptBase base base' <> "_" <> under' + EUnderover _ base under over -> + case centeredScriptOpName base of + Just op -> do + under' <- renderScriptArg ctx under + over' <- renderScriptArg ctx over + pure $ "{" <> op <> "} csub " <> centerScriptArg under' + <> " csup " <> centerScriptArg over' + Nothing -> + case limitOpName base of + Just op -> do + under' <- renderLimitArg ctx under + over' <- renderLimitArg ctx over + pure $ op <> " from " <> under' <> " to " <> over' <> " " + Nothing -> do + base' <- renderExpIn ctx base + under' <- renderScriptArg ctx under + over' <- renderScriptArg ctx over + pure $ renderScriptBase base base' <> "_" <> under' <> "^" <> over' + EArray aligns rows -> renderMatrix aligns rows + EPhantom{} -> Nothing + _ -> Nothing + +renderDelimitedBody :: AlignContext -> [Either T.Text Exp] -> Maybe T.Text +renderDelimitedBody ctx xs = do + chunks <- mapM (renderDelimitedChunk ctx) xs + pure $ T.strip (mergeDelimitedChunks chunks) + +data DelimitedChunk = DelimRaw T.Text | DelimExp Exp T.Text + +renderDelimitedChunk :: AlignContext -> Either T.Text Exp -> Maybe DelimitedChunk +renderDelimitedChunk ctx p = + case p of + Left t -> Just $ DelimRaw (" " <> delimToken DelimMiddle t <> " ") + Right x -> DelimExp x <$> renderExpIn ctx x + +mergeDelimitedChunks :: [DelimitedChunk] -> T.Text +mergeDelimitedChunks [] = "" +mergeDelimitedChunks (c0:cs) = snd $ List.foldl' step (chunkExp c0, chunkText c0) cs + where + step (prevExp, acc) cur + | T.null curText = (prevExp, acc) + | otherwise = + case cur of + DelimRaw _ -> (Nothing, acc <> curText) + DelimExp curExp _ -> + let sep = case prevExp of + Just pe -> if needsSeparator pe curExp then " " else "" + Nothing -> "" + in (Just curExp, acc <> sep <> curText) + where + curText = chunkText cur + + chunkText c = + case c of + DelimRaw t -> t + DelimExp _ t -> t + + chunkExp c = + case c of + DelimRaw _ -> Nothing + DelimExp e _ -> Just e + +renderMatrix :: [Alignment] -> [[[Exp]]] -> Maybe T.Text +renderMatrix aligns rows = do + rows' <- mapM (renderMatrixRow aligns) rows + pure $ "matrix { " <> T.intercalate " ## " rows' <> " }" + +renderMatrixRow :: [Alignment] -> [[Exp]] -> Maybe T.Text +renderMatrixRow aligns cells = do + cells' <- sequence + [ renderMatrixCellWithAlign (columnAlign aligns i) c + | (i, c) <- zip [(0 :: Int) ..] cells + ] + pure $ T.intercalate " # " cells' + +renderMatrixCell :: AlignContext -> [Exp] -> Maybe T.Text +renderMatrixCell _ [] = Just "{}" +renderMatrixCell ctx xs = do + rendered <- renderExpsIn ctx xs + let stripped = T.strip rendered + pure $ if T.null stripped then "{}" else stripped + +renderMatrixCellWithAlign :: Alignment -> [Exp] -> Maybe T.Text +renderMatrixCellWithAlign align xs = do + cell <- renderMatrixCell (alignmentContext align) xs + pure $ case align of + AlignLeft -> "alignl " <> cell + AlignRight -> "alignr " <> cell + _ -> cell + +columnAlign :: [Alignment] -> Int -> Alignment +columnAlign aligns i = + case drop i aligns of + (a : _) -> a + [] -> AlignCenter + +renderStyled :: AlignContext -> TextType -> [Exp] -> Maybe T.Text +renderStyled ctx sty xs = do + body <- renderExpsIn ctx xs + pure $ case sty of + TextNormal + | Just txt <- styledText xs -> quoteText txt + | Just txt <- renderTextNormalStyled xs + , shouldForceUprightTextNormal xs -> "nitalic{" <> txt <> "}" + | Just txt <- renderTextNormalStyled xs -> txt + TextItalic -> "ital " <> styleArg body + TextBold -> "bold " <> styleArg body + TextScript -> "ital " <> styleArg body + TextFraktur -> "ital " <> styleArg body + TextDoubleStruck -> "ital " <> styleArg body + _ -> body + where + styleArg t + | T.null t = "{}" + | T.length t == 1 = t + | otherwise = "{" <> t <> "}" + +styledText :: [Exp] -> Maybe T.Text +styledText = fmap T.concat . mapM styledTextExp + +styledTextExp :: Exp -> Maybe T.Text +styledTextExp e = + case e of + ENumber t -> Just t + EIdentifier t -> Just t + EText _ t -> Just t + ESpace w + | w <= 0 -> Just "" + | w >= 2 -> Just " " + | otherwise -> Just " " + EGrouped xs -> styledText xs + EStyled TextNormal xs -> styledText xs + ESub base sub -> do + base' <- styledTextExp base + sub' <- styledTextNonNumericExp sub + pure $ base' <> "_" <> sub' + ESuper base sup -> do + base' <- styledTextExp base + sup' <- styledTextNonNumericExp sup + pure $ base' <> "^" <> sup' + ESubsup base sub sup -> do + base' <- styledTextExp base + sub' <- styledTextNonNumericExp sub + sup' <- styledTextNonNumericExp sup + pure $ base' <> "_" <> sub' <> "^" <> sup' + ESymbol _ s + | isPlainTextSymbol s -> Just s + _ -> Nothing + +styledTextNonNumericExp :: Exp -> Maybe T.Text +styledTextNonNumericExp e = do + txt <- styledTextExp e + if T.any isAsciiDigit txt + then Nothing + else Just txt + +isAsciiDigit :: Char -> Bool +isAsciiDigit c = c >= '0' && c <= '9' + +isPlainTextSymbol :: T.Text -> Bool +isPlainTextSymbol s = + s `elem` + [ "_", ",", ".", ":", ";", "-", "−", "/", "(", ")", "[", "]" + , "+", "=", "'", "′" + ] + +renderTextNormalStyled :: [Exp] -> Maybe T.Text +renderTextNormalStyled xs = do + rendered <- mapM renderTextNormalExp xs + pure $ mergePieces (zip xs rendered) + +renderTextNormalExp :: Exp -> Maybe T.Text +renderTextNormalExp e = + case e of + ENumber t -> Just t + EIdentifier t -> Just (renderIdentifier t) + EText _ t -> Just (quoteText t) + ESpace w -> Just (renderSpace w) + EGrouped xs -> renderTextNormalStyled xs + EStyled TextNormal xs -> renderTextNormalStyled xs + EStyled TextBold xs -> do + body <- renderExpsIn AlignDefault xs + pure $ "bold nitalic " <> styleArg body + EStyled sty xs -> renderStyled AlignDefault sty xs + ESub base sub -> do + base' <- renderTextNormalExp base + sub' <- renderTextNormalExp sub + pure $ base' <> "_" <> sub' + ESuper base sup -> do + base' <- renderTextNormalExp base + sup' <- renderTextNormalExp sup + pure $ base' <> "^" <> sup' + ESubsup base sub sup -> do + base' <- renderTextNormalExp base + sub' <- renderTextNormalExp sub + sup' <- renderTextNormalExp sup + pure $ base' <> "_" <> sub' <> "^" <> sup' + ESymbol t s -> Just (T.strip (renderSymbol t s)) + _ -> Nothing + +shouldForceUprightTextNormal :: [Exp] -> Bool +shouldForceUprightTextNormal = all isUprightTextNormalExp + +isUprightTextNormalExp :: Exp -> Bool +isUprightTextNormalExp e = + case e of + ENumber{} -> True + EIdentifier{} -> True + EText{} -> True + ESpace{} -> True + ESymbol _ s -> isPlainTextSymbol s + EGrouped xs -> shouldForceUprightTextNormal xs + EStyled TextNormal xs -> shouldForceUprightTextNormal xs + ESub base sub -> isUprightTextNormalExp base && isUprightTextNormalExp sub + ESuper base sup -> isUprightTextNormalExp base && isUprightTextNormalExp sup + ESubsup base sub sup -> isUprightTextNormalExp base + && isUprightTextNormalExp sub + && isUprightTextNormalExp sup + _ -> False + +styleArg :: T.Text -> T.Text +styleArg t + | T.null t = "{}" + | T.length t == 1 = t + | otherwise = "{" <> t <> "}" + +alignmentContext :: Alignment -> AlignContext +alignmentContext a = + case a of + AlignLeft -> AlignLeftCtx + AlignRight -> AlignRightCtx + _ -> AlignDefault + +maybeCenterFractionArg :: AlignContext -> T.Text -> T.Text +maybeCenterFractionArg ctx t + | ctx == AlignLeftCtx || ctx == AlignRightCtx = "{alignc " <> asArg t <> "}" + | otherwise = t + where + asArg x = + let s = T.strip x + in if T.null s + then "{}" + else if T.length s == 1 + then s + else if T.head s == '{' && T.last s == '}' + then s + else "{" <> s <> "}" + +renderSpace :: Rational -> T.Text +renderSpace w + | w <= 0 = "" + | w >= 2 = "~~ " + | w >= 1 = "~ " + | otherwise = " " + +renderIdentifier :: T.Text -> T.Text +renderIdentifier ident = + case greekName ident of + Just name + | shouldItalicizeGreek ident -> "%i" <> name + | otherwise -> "%" <> name + Nothing -> ident + +renderMathOperator :: T.Text -> T.Text +renderMathOperator t + | isBareMathOperator t = t + | otherwise = "func " <> t + +isBareMathOperator :: T.Text -> Bool +isBareMathOperator t = + t `elem` + [ "min", "max", "log", "sin", "cos", "cosh", "sinh" + , "cot", "ln", "exp" + ] + +shouldItalicizeGreek :: T.Text -> Bool +shouldItalicizeGreek ident = + case ident of + "α" -> True + "β" -> True + "γ" -> True + "δ" -> True + "ϵ" -> True + "ε" -> True + "ζ" -> True + "η" -> True + "θ" -> True + "ϑ" -> True + "ι" -> True + "κ" -> True + "λ" -> True + "μ" -> True + "ν" -> True + "ξ" -> True + "ο" -> True + "π" -> True + "ϖ" -> True + "ρ" -> True + "ϱ" -> True + "𝜚" -> True + "σ" -> True + "ς" -> True + "𝜍" -> True + "τ" -> True + "υ" -> True + "ϕ" -> True + "φ" -> True + "χ" -> True + "ψ" -> True + "ω" -> True + _ -> False + +greekName :: T.Text -> Maybe T.Text +greekName ident = + case ident of + "α" -> Just "alpha" + "β" -> Just "beta" + "γ" -> Just "gamma" + "δ" -> Just "delta" + "ϵ" -> Just "varepsilon" + "ε" -> Just "epsilon" + "ζ" -> Just "zeta" + "η" -> Just "eta" + "θ" -> Just "theta" + "ϑ" -> Just "vartheta" + "ι" -> Just "iota" + "κ" -> Just "kappa" + "λ" -> Just "lambda" + "μ" -> Just "mu" + "ν" -> Just "nu" + "ξ" -> Just "xi" + "ο" -> Just "omicron" + "π" -> Just "pi" + "ϖ" -> Just "varpi" + "ρ" -> Just "rho" + "ϱ" -> Just "varrho" + "𝜚" -> Just "varrho" + "σ" -> Just "sigma" + "ς" -> Just "varsigma" + "𝜍" -> Just "varsigma" + "τ" -> Just "tau" + "υ" -> Just "upsilon" + "ϕ" -> Just "phi" + "φ" -> Just "varphi" + "χ" -> Just "chi" + "ψ" -> Just "psi" + "ω" -> Just "omega" + "Γ" -> Just "GAMMA" + "Δ" -> Just "DELTA" + "Θ" -> Just "THETA" + "Λ" -> Just "LAMBDA" + "Ξ" -> Just "XI" + "Π" -> Just "PI" + "Σ" -> Just "SIGMA" + "Υ" -> Just "UPSILON" + "Φ" -> Just "PHI" + "Ψ" -> Just "PSI" + "Ω" -> Just "OMEGA" + _ -> Nothing + +renderScriptBase :: Exp -> T.Text -> T.Text +renderScriptBase e rendered0 = + let rendered = T.strip rendered0 + in if isEmptyScriptBase e || T.null rendered + then "{}" + else if isAtomic e + then rendered + else "{" <> rendered <> "}" + +renderScriptArg :: AlignContext -> Exp -> Maybe T.Text +renderScriptArg ctx e = do + rendered0 <- renderExpIn ctx e + let rendered = T.strip rendered0 + pure $ if isAtomic e || isQuotedText rendered + then rendered + else "{" <> rendered <> "}" + +renderLimitArg :: AlignContext -> Exp -> Maybe T.Text +renderLimitArg ctx e = + case e of + EGrouped xs -> renderExpsIn ctx xs + _ -> T.strip <$> renderExpIn ctx e + +renderAccentArg :: Exp -> T.Text -> T.Text +renderAccentArg e rendered0 = + let rendered = T.strip rendered0 + in if isAtomic e + then rendered + else "{" <> rendered <> "}" + +centerScriptArg :: T.Text -> T.Text +centerScriptArg rendered + | isWrapped rendered = rendered + | otherwise = "{" <> rendered <> "}" + where + isWrapped t = T.length t >= 2 && T.head t == '{' && T.last t == '}' + +isAtomic :: Exp -> Bool +isAtomic e = + case e of + ENumber{} -> True + EIdentifier{} -> True + EMathOperator{} -> True + EText{} -> True + ESymbol{} -> True + _ -> False + +isEmptyScriptBase :: Exp -> Bool +isEmptyScriptBase e = + case e of + EIdentifier t -> T.null t + _ -> False + +accentName :: Exp -> Maybe T.Text +accentName e = + case e of + ESymbol Accent s -> accentFromChar s + ESymbol _ s -> accentFromChar s + _ -> Nothing + +accentFromChar :: T.Text -> Maybe T.Text +accentFromChar s = + case s of + "\775" -> Just "dot" + "˙" -> Just "dot" + "\776" -> Just "ddot" + "¨" -> Just "ddot" + "\770" -> Just "hat" + "ˆ" -> Just "hat" + "\780" -> Just "check" + "ˇ" -> Just "check" + "\771" -> Just "tilde" + "˜" -> Just "tilde" + "\772" -> Just "bar" + "\8254" -> Just "bar" + "¯" -> Just "bar" + "\8407" -> Just "vec" + "→" -> Just "vec" + "\774" -> Just "breve" + "˘" -> Just "breve" + _ -> Nothing + +data DelimSide = DelimLeft | DelimRight | DelimMiddle + +delimToken :: DelimSide -> T.Text -> T.Text +delimToken side raw = + case raw of + "" -> "none" + "." -> "none" + "(" -> "(" + ")" -> ")" + "[" -> "[" + "]" -> "]" + "{" -> case side of + DelimLeft -> "lbrace" + DelimRight -> "rbrace" + DelimMiddle -> "{" + "}" -> case side of + DelimLeft -> "lbrace" + DelimRight -> "rbrace" + DelimMiddle -> "}" + "|" -> case side of + DelimLeft -> "lline" + DelimRight -> "rline" + DelimMiddle -> "mline" + "∣" -> case side of + DelimLeft -> "lline" + DelimRight -> "rline" + DelimMiddle -> "mline" + "∥" -> case side of + DelimLeft -> "ldline" + DelimRight -> "rdline" + DelimMiddle -> "mline" + "⟨" -> "langle" + "⟩" -> "rangle" + "⌊" -> "lfloor" + "⌋" -> "rfloor" + "⌈" -> "lceil" + "⌉" -> "rceil" + "⟦" -> "ldbracket" + "⟧" -> "rdbracket" + _ -> raw + +renderSymbol :: TeXSymbolType -> T.Text -> T.Text +renderSymbol t s = + case s of + "∫" -> "int " + "∑" -> "sum " + "←" -> " leftarrow " + "→" -> " toward " + "↔" -> " leftrightarrow " + "⇐" -> " dlarrow " + "⇒" -> " drarrow " + "⇔" -> " dlrarrow " + "↑" -> " uparrow " + "↓" -> " downarrow " + "↦" -> " mapsto " + "… " -> " dotsaxis " + "…" -> " dotsaxis " + "⋯" -> " dotsaxis " + "⋮" -> " dotsvert " + "⋱" -> " dotsdown " + "⋰" -> " dotsup " + "∈" -> " in " + "∉" -> " notin " + "∋" -> " owns " + "∩" -> " intersection " + "∪" -> " union " + "⊂" -> " subset " + "⊆" -> " subseteq " + "⊃" -> " supset " + "⊇" -> " supseteq " + "≤" -> " <= " + "≥" -> " >= " + "≠" -> " <> " + "≈" -> " approx " + "≡" -> " equiv " + "∝" -> " prop " + "∥" -> " parallel " + "⊥" -> " ortho " + "±" -> " plusminus " + "∓" -> " minusplus " + "×" -> " times " + "⋅" -> " cdot " + "·" -> " cdot " + "∘" -> " circ " + "/" -> " / " + "∂" -> "partial" + "∇" -> "nabla" + "∀" -> "forall" + "∃" -> "exists" + "¬" -> "neg" + "∧" -> "and" + "∨" -> "or" + "∞" -> "infinity" + "∅" -> "emptyset" + "+" -> " + " + "-" | t == Bin -> " - " + "-" -> "-" + "−" | t == Bin -> " - " + "−" -> "-" + "=" -> " = " + "," -> ", " + ";" -> "; " + ":" -> " : " + "!" -> " ! " + "'" -> "′" + "′" -> "′" + _ -> s + +quoteText :: T.Text -> T.Text +quoteText t = "\"" <> escapeQuotes t <> "\"" + +escapeQuotes :: T.Text -> T.Text +escapeQuotes = T.replace "\"" "\\\"" + +isQuotedText :: T.Text -> Bool +isQuotedText t = + T.length t >= 2 && T.head t == '"' && T.last t == '"' + +largeOpName :: Exp -> Maybe T.Text +largeOpName e = + case e of + ESymbol Op "\8747" -> Just "int" + ESymbol Op "\8721" -> Just "sum" + ESymbol Op "\8719" -> Just "prod" + ESymbol Op "∫" -> Just "int" + ESymbol Op "∑" -> Just "sum" + ESymbol Op "∏" -> Just "prod" + _ -> Nothing + +limitOpName :: Exp -> Maybe T.Text +limitOpName e = + case largeOpName e of + Just op -> Just op + Nothing -> + case e of + EMathOperator "lim" -> Just "lim" + EMathOperator "liminf" -> Just "liminf" + EMathOperator "limsup" -> Just "limsup" + EMathOperator "min" -> Nothing + EMathOperator "max" -> Nothing + _ -> Nothing + +centeredScriptOpName :: Exp -> Maybe T.Text +centeredScriptOpName e = + case e of + EMathOperator "min" -> Just "func min" + EMathOperator "max" -> Just "func max" + _ -> Nothing diff --git a/test/test-texmath.hs b/test/test-texmath.hs index 70ff912e..68a658e7 100644 --- a/test/test-texmath.hs +++ b/test/test-texmath.hs @@ -37,6 +37,7 @@ main = do mmlWriterTests <- getFiles "test/writer/mml" ommlWriterTests <- getFiles "test/writer/omml" eqnWriterTests <- getFiles "test/writer/eqn" + starmathWriterTests <- getFiles "test/writer/starmath" typstWriterTests <- getFiles "test/writer/typst" regressionTests <- getFiles "test/regression" roundtripTests <- getFiles "test/roundtrip" @@ -63,6 +64,7 @@ main = do , testGroup "mml" $ map toGoldenTest mmlWriterTests , testGroup "omml" $ map toGoldenTest ommlWriterTests , testGroup "eqn" $ map toGoldenTest eqnWriterTests + , testGroup "starmath" $ map toGoldenTest starmathWriterTests , testGroup "typst" $ map toGoldenTest typstWriterTests ], testGroup "regression" $ map toGoldenTest regressionTests @@ -148,6 +150,7 @@ writers = [ ("mml", T.pack . ppTopElement . writeMathML DisplayBlock) , ("tex", writeTeX) , ("omml", T.pack . ppTopElement . writeOMML DisplayBlock) , ("eqn", writeEqn DisplayBlock) + , ("starmath", writeStarMath DisplayBlock) , ("typst", writeTypst DisplayBlock) , ("native", T.pack . ppShow) , ("pandoc", maybe "" (T.pack . ppShow) . writePandoc DisplayBlock) diff --git a/test/writer/starmath/001_dot_text_subscript.test b/test/writer/starmath/001_dot_text_subscript.test new file mode 100644 index 00000000..80313bf3 --- /dev/null +++ b/test/writer/starmath/001_dot_text_subscript.test @@ -0,0 +1,4 @@ +<<< tex +\dot{Q}_{\text{dem}}(t)=\dot{Q}_{\text{eb}}(t)+\dot{Q}_{\text{dis}}(t)+\dot{Q}_{\text{boil}}(t), +>>> starmath +{dot Q}_"dem"(t) = {dot Q}_"eb"(t) + {dot Q}_"dis"(t) + {dot Q}_"boil"(t), diff --git a/test/writer/starmath/002_common_accents.test b/test/writer/starmath/002_common_accents.test new file mode 100644 index 00000000..af2fe03e --- /dev/null +++ b/test/writer/starmath/002_common_accents.test @@ -0,0 +1,4 @@ +<<< tex +\ddot{x}+\hat{x}+\tilde{x}+\vec{x}+\bar{x} +>>> starmath +ddot x + hat x + tilde x + vec x + bar x diff --git a/test/writer/starmath/003_fraction.test b/test/writer/starmath/003_fraction.test new file mode 100644 index 00000000..d241a5df --- /dev/null +++ b/test/writer/starmath/003_fraction.test @@ -0,0 +1,4 @@ +<<< tex +\frac{a+b}{c} +>>> starmath +{{a + b} over c} diff --git a/test/writer/starmath/004_square_root.test b/test/writer/starmath/004_square_root.test new file mode 100644 index 00000000..5259a744 --- /dev/null +++ b/test/writer/starmath/004_square_root.test @@ -0,0 +1,4 @@ +<<< tex +\sqrt{x+1} +>>> starmath +sqrt {{x + 1}} diff --git a/test/writer/starmath/005_nth_root.test b/test/writer/starmath/005_nth_root.test new file mode 100644 index 00000000..ccdd160c --- /dev/null +++ b/test/writer/starmath/005_nth_root.test @@ -0,0 +1,4 @@ +<<< tex +\sqrt[3]{x} +>>> starmath +nroot {3} {x} diff --git a/test/writer/starmath/006_subscript_and_superscript.test b/test/writer/starmath/006_subscript_and_superscript.test new file mode 100644 index 00000000..9a266789 --- /dev/null +++ b/test/writer/starmath/006_subscript_and_superscript.test @@ -0,0 +1,4 @@ +<<< tex +x_i^2 +>>> starmath +x_i^2 diff --git a/test/writer/starmath/007_superscript_with_grouped_base.test b/test/writer/starmath/007_superscript_with_grouped_base.test new file mode 100644 index 00000000..e4a1f9a1 --- /dev/null +++ b/test/writer/starmath/007_superscript_with_grouped_base.test @@ -0,0 +1,4 @@ +<<< tex +(a+b)^2 +>>> starmath +(a + b)^2 diff --git a/test/writer/starmath/008_delimited_fraction.test b/test/writer/starmath/008_delimited_fraction.test new file mode 100644 index 00000000..6ad0847e --- /dev/null +++ b/test/writer/starmath/008_delimited_fraction.test @@ -0,0 +1,4 @@ +<<< tex +\left(\frac{a}{b}\right) +>>> starmath +left ( {a over b} right ) diff --git a/test/writer/starmath/009_delimited_braces.test b/test/writer/starmath/009_delimited_braces.test new file mode 100644 index 00000000..40e22436 --- /dev/null +++ b/test/writer/starmath/009_delimited_braces.test @@ -0,0 +1,4 @@ +<<< tex +\left\{\frac{a}{b}\right\} +>>> starmath +left lbrace {a over b} right rbrace diff --git a/test/writer/starmath/010_absolute_value_bars_normalize_to_delimiters.test b/test/writer/starmath/010_absolute_value_bars_normalize_to_delimiters.test new file mode 100644 index 00000000..c6f01fb0 --- /dev/null +++ b/test/writer/starmath/010_absolute_value_bars_normalize_to_delimiters.test @@ -0,0 +1,4 @@ +<<< tex +|r_i| \le \varepsilon_{\mathrm{free}} +>>> starmath +left lline r_i right rline <= %iepsilon_"free" diff --git a/test/writer/starmath/011_double_bars_normalize_to_norm_delimiters.test b/test/writer/starmath/011_double_bars_normalize_to_norm_delimiters.test new file mode 100644 index 00000000..58b2b71e --- /dev/null +++ b/test/writer/starmath/011_double_bars_normalize_to_norm_delimiters.test @@ -0,0 +1,4 @@ +<<< tex +\|\mathbf{A}\mathbf{n} - \mathbf{b}\| +>>> starmath +left ldline bold nitalic A bold nitalic n - bold nitalic b right rdline diff --git a/test/writer/starmath/012_one_sided_delimiter_uses_none.test b/test/writer/starmath/012_one_sided_delimiter_uses_none.test new file mode 100644 index 00000000..562622b9 --- /dev/null +++ b/test/writer/starmath/012_one_sided_delimiter_uses_none.test @@ -0,0 +1,4 @@ +<<< tex +\left. x \right| +>>> starmath +left none x right rline diff --git a/test/writer/starmath/013_middle_delimiter_uses_mline.test b/test/writer/starmath/013_middle_delimiter_uses_mline.test new file mode 100644 index 00000000..b8163dcd --- /dev/null +++ b/test/writer/starmath/013_middle_delimiter_uses_mline.test @@ -0,0 +1,4 @@ +<<< tex +\left( x \middle| y \right) +>>> starmath +left ( x mline y right ) diff --git a/test/writer/starmath/014_operator_mapping_cdot.test b/test/writer/starmath/014_operator_mapping_cdot.test new file mode 100644 index 00000000..d0b61a70 --- /dev/null +++ b/test/writer/starmath/014_operator_mapping_cdot.test @@ -0,0 +1,4 @@ +<<< tex +a\cdot b +>>> starmath +a cdot b diff --git a/test/writer/starmath/015_binomial_nolinefrac.test b/test/writer/starmath/015_binomial_nolinefrac.test new file mode 100644 index 00000000..bf2ef74f --- /dev/null +++ b/test/writer/starmath/015_binomial_nolinefrac.test @@ -0,0 +1,4 @@ +<<< tex +\binom{n}{k} +>>> starmath +left ( {n / k} right ) diff --git a/test/writer/starmath/016_integral_without_limits.test b/test/writer/starmath/016_integral_without_limits.test new file mode 100644 index 00000000..037d6461 --- /dev/null +++ b/test/writer/starmath/016_integral_without_limits.test @@ -0,0 +1,4 @@ +<<< tex +\int x\,dx +>>> starmath +int x dx diff --git a/test/writer/starmath/017_integral_with_lower_and_upper_limits.test b/test/writer/starmath/017_integral_with_lower_and_upper_limits.test new file mode 100644 index 00000000..0941d854 --- /dev/null +++ b/test/writer/starmath/017_integral_with_lower_and_upper_limits.test @@ -0,0 +1,4 @@ +<<< tex +\int_0^1 x\,dx +>>> starmath +int from 0 to 1 x dx diff --git a/test/writer/starmath/018_integral_with_infinite_upper_limit.test b/test/writer/starmath/018_integral_with_infinite_upper_limit.test new file mode 100644 index 00000000..23c5a239 --- /dev/null +++ b/test/writer/starmath/018_integral_with_infinite_upper_limit.test @@ -0,0 +1,4 @@ +<<< tex +\int_{0}^{\infty} e^{-x}\,dx +>>> starmath +int from 0 to infinity e^{{- x}} dx diff --git a/test/writer/starmath/019_sum_with_lower_and_upper_limits.test b/test/writer/starmath/019_sum_with_lower_and_upper_limits.test new file mode 100644 index 00000000..e7b73de1 --- /dev/null +++ b/test/writer/starmath/019_sum_with_lower_and_upper_limits.test @@ -0,0 +1,4 @@ +<<< tex +\sum_{i=1}^{n} i +>>> starmath +sum from i = 1 to n i diff --git a/test/writer/starmath/020_sum_with_symbolic_term.test b/test/writer/starmath/020_sum_with_symbolic_term.test new file mode 100644 index 00000000..4a1ddf48 --- /dev/null +++ b/test/writer/starmath/020_sum_with_symbolic_term.test @@ -0,0 +1,4 @@ +<<< tex +\sum_{k=1}^{n} a_k +>>> starmath +sum from k = 1 to n a_k diff --git a/test/writer/starmath/021_greek_letter_mapping.test b/test/writer/starmath/021_greek_letter_mapping.test new file mode 100644 index 00000000..67823475 --- /dev/null +++ b/test/writer/starmath/021_greek_letter_mapping.test @@ -0,0 +1,4 @@ +<<< tex +\alpha + \beta + \Gamma + \Omega +>>> starmath +%ialpha + %ibeta + %GAMMA + %OMEGA diff --git a/test/writer/starmath/022_greek_variant_mapping.test b/test/writer/starmath/022_greek_variant_mapping.test new file mode 100644 index 00000000..6bcdbd01 --- /dev/null +++ b/test/writer/starmath/022_greek_variant_mapping.test @@ -0,0 +1,4 @@ +<<< tex +\phi + \varphi + \epsilon + \varepsilon + \vartheta +>>> starmath +%iphi + %ivarphi + %ivarepsilon + %iepsilon + %ivartheta diff --git a/test/writer/starmath/023_arrow_mapping.test b/test/writer/starmath/023_arrow_mapping.test new file mode 100644 index 00000000..253e3b62 --- /dev/null +++ b/test/writer/starmath/023_arrow_mapping.test @@ -0,0 +1,4 @@ +<<< tex +x \to y, x \leftarrow y, x \Rightarrow y, x \Leftrightarrow y +>>> starmath +x toward y, x leftarrow y, x drarrow y, x dlrarrow y diff --git a/test/writer/starmath/024_set_and_relation_symbol_mapping.test b/test/writer/starmath/024_set_and_relation_symbol_mapping.test new file mode 100644 index 00000000..fc59b7a7 --- /dev/null +++ b/test/writer/starmath/024_set_and_relation_symbol_mapping.test @@ -0,0 +1,4 @@ +<<< tex +A \subseteq B, A \cup B, x \in A, x \notin B +>>> starmath +A subseteq B, A union B, x in A, x notin B diff --git a/test/writer/starmath/025_logic_and_calculus_symbol_mapping.test b/test/writer/starmath/025_logic_and_calculus_symbol_mapping.test new file mode 100644 index 00000000..8930eecb --- /dev/null +++ b/test/writer/starmath/025_logic_and_calculus_symbol_mapping.test @@ -0,0 +1,4 @@ +<<< tex +\forall x \exists y, \nabla f = 0, \partial_t u +>>> starmath +forall x exists y, nabla f = 0, partial_t u diff --git a/test/writer/starmath/026_greek_identifier_spacing_in_products.test b/test/writer/starmath/026_greek_identifier_spacing_in_products.test new file mode 100644 index 00000000..4afc5ebc --- /dev/null +++ b/test/writer/starmath/026_greek_identifier_spacing_in_products.test @@ -0,0 +1,4 @@ +<<< tex +E_{k+1}=E_{k+\frac12}-\frac{\dot{Q}_{\text{dis},k}\Delta t}{\eta_{\text{dis}}}, +>>> starmath +E_{{k + 1}} = E_{{k + {1 over 2}}} - {{{dot Q}_{{"dis", k}} %DELTA t} over %ieta_"dis"}, diff --git a/test/writer/starmath/027_math_operators_rendered_as_functions.test b/test/writer/starmath/027_math_operators_rendered_as_functions.test new file mode 100644 index 00000000..32f28f8a --- /dev/null +++ b/test/writer/starmath/027_math_operators_rendered_as_functions.test @@ -0,0 +1,4 @@ +<<< tex +E_{k+1}\leftarrow\min\!\left(E^{\text{cap}}_{\text{s}},\max(0,E_{k+1})\right). +>>> starmath +E_{{k + 1}} leftarrow min left ( E_"s"^"cap", max(0, E_{{k + 1}}) right ). diff --git a/test/writer/starmath/028_nested_function_with_left_delimiter_spacing.test b/test/writer/starmath/028_nested_function_with_left_delimiter_spacing.test new file mode 100644 index 00000000..4b209273 --- /dev/null +++ b/test/writer/starmath/028_nested_function_with_left_delimiter_spacing.test @@ -0,0 +1,4 @@ +<<< tex +P_{\text{ch}, k}=\max\!\left(0,\min\!\left(P'_{\text{pv}, k},\,P^{\text{cap}}_{\text{ch}},\,P_{\text{ch,head}, k}\right)\right). +>>> starmath +P_{{"ch", k}} = max left ( 0, min left ( P_{{"pv", k}}^′, P_"ch"^"cap", P_{{"ch,head", k}} right ) right ). diff --git a/test/writer/starmath/029_quad_spacing_command.test b/test/writer/starmath/029_quad_spacing_command.test new file mode 100644 index 00000000..c53475af --- /dev/null +++ b/test/writer/starmath/029_quad_spacing_command.test @@ -0,0 +1,4 @@ +<<< tex +a,\quad b +>>> starmath +a, ~ b diff --git a/test/writer/starmath/030_qquad_spacing_command.test b/test/writer/starmath/030_qquad_spacing_command.test new file mode 100644 index 00000000..2b9f5896 --- /dev/null +++ b/test/writer/starmath/030_qquad_spacing_command.test @@ -0,0 +1,4 @@ +<<< tex +a,\qquad b +>>> starmath +a, ~~ b diff --git a/test/writer/starmath/031_greek_token_separator_before_scripted_identifier.test b/test/writer/starmath/031_greek_token_separator_before_scripted_identifier.test new file mode 100644 index 00000000..53ddfdc2 --- /dev/null +++ b/test/writer/starmath/031_greek_token_separator_before_scripted_identifier.test @@ -0,0 +1,4 @@ +<<< tex +f_{\text{eff}}=(1-\lambda)f_{\text{m}}+\lambda f_{\text{l}}. +>>> starmath +f_"eff" = (1 - %ilambda) f_"m" + %ilambda f_"l". diff --git a/test/writer/starmath/032_mathrm_text_in_subscripts_stays_quoted.test b/test/writer/starmath/032_mathrm_text_in_subscripts_stays_quoted.test new file mode 100644 index 00000000..0e1244bc --- /dev/null +++ b/test/writer/starmath/032_mathrm_text_in_subscripts_stays_quoted.test @@ -0,0 +1,4 @@ +<<< tex +n_{\mathrm{Fe,metal}} = n_{\mathrm{Fe_bcc}} + n_{\mathrm{Fe_fcc}} +>>> starmath +n_"Fe,metal" = n_"Fe_bcc" + n_"Fe_fcc" diff --git a/test/writer/starmath/033_escaped_underscore_in_mathrm_stays_literal.test b/test/writer/starmath/033_escaped_underscore_in_mathrm_stays_literal.test new file mode 100644 index 00000000..8295c840 --- /dev/null +++ b/test/writer/starmath/033_escaped_underscore_in_mathrm_stays_literal.test @@ -0,0 +1,4 @@ +<<< tex +n_{\mathrm{wus}} = n_{\mathrm{Wus\_FeO}} + n_{\mathrm{Wus\_FeO1p5}} +>>> starmath +n_"wus" = n_"Wus_FeO" + n_"Wus_FeO1p5" diff --git a/test/writer/starmath/034_numeric_chemistry_subscripts_in_mathrm_stay_structural.test b/test/writer/starmath/034_numeric_chemistry_subscripts_in_mathrm_stay_structural.test new file mode 100644 index 00000000..5144f610 --- /dev/null +++ b/test/writer/starmath/034_numeric_chemistry_subscripts_in_mathrm_stay_structural.test @@ -0,0 +1,4 @@ +<<< tex +\frac{n_{\mathrm{H_2O}}}{n_{\mathrm{H_2}}}\qquad \text{or} \qquad \frac{p_{\mathrm{H_2O}}}{p_{\mathrm{H_2}}} +>>> starmath +{n_{nitalic{H_2 O}} over n_{nitalic{H_2}}}~~ "or"~~ {p_{nitalic{H_2 O}} over p_{nitalic{H_2}}} diff --git a/test/writer/starmath/035_circ_operator_maps_to_starmath_command.test b/test/writer/starmath/035_circ_operator_maps_to_starmath_command.test new file mode 100644 index 00000000..a5fe9416 --- /dev/null +++ b/test/writer/starmath/035_circ_operator_maps_to_starmath_command.test @@ -0,0 +1,4 @@ +<<< tex +\mu_i^\circ(T, P^\circ) +>>> starmath +%imu_i^circ(T, P^circ) diff --git a/test/writer/starmath/036_min_with_bold_limit_variable_uses_operator_limits.test b/test/writer/starmath/036_min_with_bold_limit_variable_uses_operator_limits.test new file mode 100644 index 00000000..29eaca4f --- /dev/null +++ b/test/writer/starmath/036_min_with_bold_limit_variable_uses_operator_limits.test @@ -0,0 +1,4 @@ +<<< tex +\min_{\mathbf{n}}\; G(\mathbf{n}) +>>> starmath +{func min} csub {bold nitalic n} G(bold nitalic n) diff --git a/test/writer/starmath/037_identifier_before_function_gets_separator.test b/test/writer/starmath/037_identifier_before_function_gets_separator.test new file mode 100644 index 00000000..616854b3 --- /dev/null +++ b/test/writer/starmath/037_identifier_before_function_gets_separator.test @@ -0,0 +1,4 @@ +<<< tex +\mu_i(T,P,\mathbf{n}) = \mu_i^\circ(T,P^\circ) + RT\ln\left(\frac{y_i P}{P^\circ}\right) +>>> starmath +%imu_i(T, P, bold nitalic n) = %imu_i^circ(T, P^circ) + RT ln left ( {{y_i P} over P^circ} right ) diff --git a/test/writer/starmath/038_greek_identifier_before_punctuation_gets_separator.test b/test/writer/starmath/038_greek_identifier_before_punctuation_gets_separator.test new file mode 100644 index 00000000..d4e5c69f --- /dev/null +++ b/test/writer/starmath/038_greek_identifier_before_punctuation_gets_separator.test @@ -0,0 +1,4 @@ +<<< tex +\xi. +>>> starmath +%ixi . diff --git a/test/writer/starmath/039_greek_identifier_before_punctuation_in_larger_expression.test b/test/writer/starmath/039_greek_identifier_before_punctuation_in_larger_expression.test new file mode 100644 index 00000000..eb8a29c1 --- /dev/null +++ b/test/writer/starmath/039_greek_identifier_before_punctuation_in_larger_expression.test @@ -0,0 +1,4 @@ +<<< tex +(\mathbf{H}+\lambda\mathbf{I})\Delta\mathbf{z} = -\nabla\phi. +>>> starmath +(bold nitalic H + %ilambda bold nitalic I) %DELTA bold nitalic z = -nabla %iphi . diff --git a/test/writer/starmath/040_bare_recognized_math_operators_omit_func_and_keep_spacing.test b/test/writer/starmath/040_bare_recognized_math_operators_omit_func_and_keep_spacing.test new file mode 100644 index 00000000..a472a3c7 --- /dev/null +++ b/test/writer/starmath/040_bare_recognized_math_operators_omit_func_and_keep_spacing.test @@ -0,0 +1,4 @@ +<<< tex +\min x\log x\operatorname{log} x\sin x\cos x\max x\cosh x\sinh x\cot x\ln x\exp x +>>> starmath +min x log x log x sin x cos x max x cosh x sinh x cot x ln x exp x diff --git a/test/writer/starmath/041_ordinary_function_with_subscript_keeps_ordinary_script_syntax.test b/test/writer/starmath/041_ordinary_function_with_subscript_keeps_ordinary_script_syntax.test new file mode 100644 index 00000000..ed0e90fd --- /dev/null +++ b/test/writer/starmath/041_ordinary_function_with_subscript_keeps_ordinary_script_syntax.test @@ -0,0 +1,4 @@ +<<< tex +\Delta\log_{10}K_j = \log_{10}K_{j,\mathrm{FPROPS}} - \log_{10}K_{j,\mathrm{ref}},\qquad j=1,\dots,r. +>>> starmath +%DELTA log_10 K_j = log_10 K_{{j, "FPROPS"}} - log_10 K_{{j, "ref"}}, ~~ j = 1, dotsaxis , r. diff --git a/test/writer/starmath/042_adjacent_bold_terms_get_separator.test b/test/writer/starmath/042_adjacent_bold_terms_get_separator.test new file mode 100644 index 00000000..251ec00f --- /dev/null +++ b/test/writer/starmath/042_adjacent_bold_terms_get_separator.test @@ -0,0 +1,4 @@ +<<< tex +\mathcal{L}(\mathbf{n},\boldsymbol\lambda,\mathbf{s}) = G(\mathbf{n}) + \boldsymbol\lambda^T(\mathbf{A}\mathbf{n}-\mathbf{b}) - \mathbf{s}^T\mathbf{n}. +>>> starmath +ital L(bold nitalic n, bold {%ilambda}, bold nitalic s) = G(bold nitalic n) + {bold {%ilambda}}^T(bold nitalic A bold nitalic n - bold nitalic b) - {bold nitalic s}^T bold nitalic n. diff --git a/test/writer/starmath/043_mathcal_styled_token_before_left_delimiter.test b/test/writer/starmath/043_mathcal_styled_token_before_left_delimiter.test new file mode 100644 index 00000000..389578dd --- /dev/null +++ b/test/writer/starmath/043_mathcal_styled_token_before_left_delimiter.test @@ -0,0 +1,4 @@ +<<< tex +\min\ \mathcal{J}\left(P^{\text{cap}}_{\text{pv}},N_{\text{u}},P^{\text{cap}}_{\text{eb}}\right) +>>> starmath +min ital J left ( P_"pv"^"cap", N_"u", P_"eb"^"cap" right ) diff --git a/test/writer/starmath/044_leading_binary_operator_gets_neutral_lhs.test b/test/writer/starmath/044_leading_binary_operator_gets_neutral_lhs.test new file mode 100644 index 00000000..7eddd3c0 --- /dev/null +++ b/test/writer/starmath/044_leading_binary_operator_gets_neutral_lhs.test @@ -0,0 +1,4 @@ +<<< tex +\times\Delta t +>>> starmath +{} times %DELTA t diff --git a/test/writer/starmath/045_standalone_binary_operator_gets_neutral_operands.test b/test/writer/starmath/045_standalone_binary_operator_gets_neutral_operands.test new file mode 100644 index 00000000..a5e94904 --- /dev/null +++ b/test/writer/starmath/045_standalone_binary_operator_gets_neutral_operands.test @@ -0,0 +1,4 @@ +<<< tex +\times +>>> starmath +{} times {} diff --git a/test/writer/starmath/046_trailing_binary_operator_gets_neutral_rhs.test b/test/writer/starmath/046_trailing_binary_operator_gets_neutral_rhs.test new file mode 100644 index 00000000..ecd076a1 --- /dev/null +++ b/test/writer/starmath/046_trailing_binary_operator_gets_neutral_rhs.test @@ -0,0 +1,4 @@ +<<< tex +a\times +>>> starmath +a times {} diff --git a/test/writer/starmath/047_standalone_relation_gets_neutral_operands.test b/test/writer/starmath/047_standalone_relation_gets_neutral_operands.test new file mode 100644 index 00000000..c3c33a59 --- /dev/null +++ b/test/writer/starmath/047_standalone_relation_gets_neutral_operands.test @@ -0,0 +1,4 @@ +<<< tex +\leq +>>> starmath +{} <= {} diff --git a/test/writer/starmath/048_standalone_arrow_gets_neutral_operands.test b/test/writer/starmath/048_standalone_arrow_gets_neutral_operands.test new file mode 100644 index 00000000..1ab3c84b --- /dev/null +++ b/test/writer/starmath/048_standalone_arrow_gets_neutral_operands.test @@ -0,0 +1,4 @@ +<<< tex +\rightarrow +>>> starmath +{} toward {} diff --git a/test/writer/starmath/049_subscript_only_fragment_gets_neutral_base.test b/test/writer/starmath/049_subscript_only_fragment_gets_neutral_base.test new file mode 100644 index 00000000..14c1f754 --- /dev/null +++ b/test/writer/starmath/049_subscript_only_fragment_gets_neutral_base.test @@ -0,0 +1,4 @@ +<<< tex +_2 +>>> starmath +{}_2 diff --git a/test/writer/starmath/050_superscript_only_fragment_gets_neutral_base.test b/test/writer/starmath/050_superscript_only_fragment_gets_neutral_base.test new file mode 100644 index 00000000..abadd5e4 --- /dev/null +++ b/test/writer/starmath/050_superscript_only_fragment_gets_neutral_base.test @@ -0,0 +1,4 @@ +<<< tex +^+ +>>> starmath +{}^+ diff --git a/test/writer/starmath/051_grouped_subscript_only_fragment_gets_neutral_base.test b/test/writer/starmath/051_grouped_subscript_only_fragment_gets_neutral_base.test new file mode 100644 index 00000000..9a0db3ff --- /dev/null +++ b/test/writer/starmath/051_grouped_subscript_only_fragment_gets_neutral_base.test @@ -0,0 +1,4 @@ +<<< tex +_{j,\mathrm{ref}} +>>> starmath +{}_{{j, "ref"}} diff --git a/test/writer/starmath/052_matrix_environment.test b/test/writer/starmath/052_matrix_environment.test new file mode 100644 index 00000000..1cb9f429 --- /dev/null +++ b/test/writer/starmath/052_matrix_environment.test @@ -0,0 +1,4 @@ +<<< tex +\begin{matrix}a&b\\c&d\end{matrix} +>>> starmath +matrix { a # b ## c # d } diff --git a/test/writer/starmath/053_pmatrix_environment.test b/test/writer/starmath/053_pmatrix_environment.test new file mode 100644 index 00000000..1bb7a1d6 --- /dev/null +++ b/test/writer/starmath/053_pmatrix_environment.test @@ -0,0 +1,4 @@ +<<< tex +\begin{pmatrix}a&b\\c&d\end{pmatrix} +>>> starmath +left ( matrix { a # b ## c # d } right ) diff --git a/test/writer/starmath/054_bmatrix_environment.test b/test/writer/starmath/054_bmatrix_environment.test new file mode 100644 index 00000000..703e7e08 --- /dev/null +++ b/test/writer/starmath/054_bmatrix_environment.test @@ -0,0 +1,4 @@ +<<< tex +\begin{bmatrix}a&b\\c&d\end{bmatrix} +>>> starmath +left [ matrix { a # b ## c # d } right ] diff --git a/test/writer/starmath/055_vmatrix_environment.test b/test/writer/starmath/055_vmatrix_environment.test new file mode 100644 index 00000000..76870961 --- /dev/null +++ b/test/writer/starmath/055_vmatrix_environment.test @@ -0,0 +1,4 @@ +<<< tex +\begin{vmatrix}a&b\\c&d\end{vmatrix} +>>> starmath +left lline matrix { a # b ## c # d } right rline diff --git a/test/writer/starmath/056_vmatrix_environment.test b/test/writer/starmath/056_vmatrix_environment.test new file mode 100644 index 00000000..ccd542a0 --- /dev/null +++ b/test/writer/starmath/056_vmatrix_environment.test @@ -0,0 +1,4 @@ +<<< tex +\begin{Vmatrix}a&b\\c&d\end{Vmatrix} +>>> starmath +left ldline matrix { a # b ## c # d } right rdline diff --git a/test/writer/starmath/057_array_with_left_right_alignment.test b/test/writer/starmath/057_array_with_left_right_alignment.test new file mode 100644 index 00000000..0c437801 --- /dev/null +++ b/test/writer/starmath/057_array_with_left_right_alignment.test @@ -0,0 +1,4 @@ +<<< tex +\begin{array}{lr}a&b\\c&d\end{array} +>>> starmath +matrix { alignl a # alignr b ## alignl c # alignr d } diff --git a/test/writer/starmath/058_aligned_array_keeps_fractions_centered.test b/test/writer/starmath/058_aligned_array_keeps_fractions_centered.test new file mode 100644 index 00000000..748095ac --- /dev/null +++ b/test/writer/starmath/058_aligned_array_keeps_fractions_centered.test @@ -0,0 +1,4 @@ +<<< tex +\begin{array}{l}\frac{AAA}{B}\end{array} +>>> starmath +matrix { alignl {{alignc {AAA}} over {alignc B}} } diff --git a/test/writer/starmath/059_cases_environment.test b/test/writer/starmath/059_cases_environment.test new file mode 100644 index 00000000..ab9e2cdf --- /dev/null +++ b/test/writer/starmath/059_cases_environment.test @@ -0,0 +1,4 @@ +<<< tex +\begin{cases}a, & x>0\\ b, & x\le 0\end{cases} +>>> starmath +left lbrace matrix { alignl a, # alignl x>0 ## alignl b, # alignl x <= 0 } right none diff --git a/test/writer/starmath/060_cases_with_negative_log_fraction.test b/test/writer/starmath/060_cases_with_negative_log_fraction.test new file mode 100644 index 00000000..8143616b --- /dev/null +++ b/test/writer/starmath/060_cases_with_negative_log_fraction.test @@ -0,0 +1,4 @@ +<<< tex +t_{\text{pb,disc}}=\begin{cases}\dfrac{C_{\text{tot}}}{B}, & r=0,\\\dfrac{-\ln\!\left(1-rC_{\text{tot}}/B\right)}{\ln(1+r)}, & r>0\text{ and }1-rC_{\text{tot}}/B>0,\\\infty, & \text{otherwise}.\end{cases} +>>> starmath +t_"pb,disc" = left lbrace matrix { alignl {{alignc {C_"tot"}} over {alignc B}}, # alignl r = 0, ## alignl {{alignc {- ln left ( 1 - rC_"tot" / B right )}} over {alignc {ln(1 + r)}}}, # alignl r>0" and "1 - rC_"tot" / B>0, ## alignl infinity, # alignl "otherwise". } right none diff --git a/test/writer/starmath/061_fallback_to_tex_for_unsupported_forms.test b/test/writer/starmath/061_fallback_to_tex_for_unsupported_forms.test new file mode 100644 index 00000000..f87f9dbe --- /dev/null +++ b/test/writer/starmath/061_fallback_to_tex_for_unsupported_forms.test @@ -0,0 +1,4 @@ +<<< tex +\phantom{x}+1 +>>> starmath +\phantom{x} + 1 diff --git a/test/writer/starmath/062_fallback_to_tex_for_under_over_constructs.test b/test/writer/starmath/062_fallback_to_tex_for_under_over_constructs.test new file mode 100644 index 00000000..e3388be9 --- /dev/null +++ b/test/writer/starmath/062_fallback_to_tex_for_under_over_constructs.test @@ -0,0 +1,4 @@ +<<< tex +\underbrace{x+y}_{z}+\overbrace{x+y}^{z} +>>> starmath +\underset{z}{\underbrace{x + y}} + \overset{z}{\overbrace{x + y}} diff --git a/texmath.cabal b/texmath.cabal index 3965b0b0..502aea4d 100644 --- a/texmath.cabal +++ b/texmath.cabal @@ -43,6 +43,7 @@ Extra-source-files: README.md test/writer/mml/*.test test/writer/omml/*.test test/writer/tex/*.test + test/writer/starmath/*.test test/writer/typst/*.test test/writer/eqn/*.test test/reader/mml/*.test @@ -96,6 +97,7 @@ Library Text.TeXMath.Writers.OMML, Text.TeXMath.Writers.Pandoc, Text.TeXMath.Writers.TeX, + Text.TeXMath.Writers.StarMath, Text.TeXMath.Writers.Typst, Text.TeXMath.Writers.Eqn, Text.TeXMath.Unicode.ToUnicode,