content: {-# LANGUAGE OverloadedStrings #-}
  {- |
     Module      : Tests.Readers.LaTeX
     Copyright   : © 2006-2024 John MacFarlane
     License     : GNU GPL, version 2 or above
  
     Maintainer  : John MacFarlane <jgm@berkeley.edu>
     Stability   : alpha
     Portability : portable
  
  Tests for the LaTeX reader.
  -}
  module Tests.Readers.LaTeX (tests) where
  
  import Data.Text (Text)
  import qualified Data.Text as T
  import Test.Tasty
  import Test.Tasty.HUnit (HasCallStack)
  import Tests.Helpers
  import Text.Pandoc
  import Text.Pandoc.Arbitrary ()
  import Text.Pandoc.Builder
  
  latex :: Text -> Pandoc
  latex = purely $ readLaTeX def{
                     readerExtensions = getDefaultExtensions "latex" }
  
  infix 4 =:
  (=:) :: (ToString c, HasCallStack)
       => String -> (Text, c) -> TestTree
  (=:) = test latex
  
  table' :: [Alignment] -> [Row] -> Blocks
  table' aligns rows
    = table emptyCaption
            (zip aligns (repeat ColWidthDefault))
            (TableHead nullAttr [])
            [TableBody nullAttr 0 [] rows]
            (TableFoot nullAttr [])
  
  simpleTable' :: [Alignment] -> [[Blocks]] -> Blocks
  simpleTable' aligns rows
    = table' aligns (map toRow rows)
    where
      toRow = Row nullAttr . map simpleCell
  
  tests :: [TestTree]
  tests = [ testGroup "basic"
            [ "simple" =:
              "word" =?> para "word"
            , "space" =:
              "some text" =?> para "some text"
            , "emphasized" =:
              "\\emph{emphasized}" =?> para (emph "emphasized")
            ]
  
          , testGroup "headers"
            [ "level 1" =:
              "\\section{header}" =?> headerWith ("header",[],[]) 1 "header"
            , "level 2" =:
              "\\subsection{header}" =?> headerWith ("header",[],[]) 2 "header"
            , "level 3" =:
              "\\subsubsection{header}" =?> headerWith ("header",[],[]) 3 "header"
            , "emph" =:
              "\\section{text \\emph{emph}}" =?>
               headerWith ("text-emph",[],[]) 1 ("text" <> space <> emph "emph")
            , "link" =:
              "\\section{text \\href{/url}{link}}" =?>
                headerWith ("text-link",[],[]) 1 ("text" <> space <> link "/url" "" "link")
            ]
  
          , testGroup "math"
            [ "escaped $" =:
              "$x=\\$4$" =?> para (math "x=\\$4")
            ]
  
          , testGroup "space and comments"
            [ "blank lines + space at beginning" =:
              "\n  \n  hi" =?> para "hi"
            , "blank lines + space + comments" =:
              "% my comment\n\n  \n  % another\n\nhi" =?> para "hi"
            , "comment in paragraph" =:
              "hi % this is a comment\nthere\n" =?>
                  para ("hi" <> softbreak <> "there")
            ]
  
          , testGroup "code blocks"
            [ "identifier" =:
              "\\begin{lstlisting}[label=test]\\end{lstlisting}" =?> codeBlockWith ("test", [], [("label","test")]) ""
            , "no identifier" =:
              "\\begin{lstlisting}\\end{lstlisting}" =?> codeBlock ""
            ]
  
          , testGroup "tables"
            [ "Single cell table" =:
              "\\begin{tabular}{|l|}Test\\\\\\end{tabular}" =?>
              simpleTable' [AlignLeft] [[plain "Test"]]
            , "Multi cell table" =:
              "\\begin{tabular}{|rl|}One & Two\\\\ \\end{tabular}" =?>
              simpleTable' [AlignRight,AlignLeft] [[plain "One", plain "Two"]]
            , "Multi line table" =:
              T.unlines [ "\\begin{tabular}{|c|}"
                      , "One\\\\"
                      , "Two\\\\"
                      , "Three\\\\"
                      , "\\end{tabular}" ] =?>
              simpleTable' [AlignCenter]
                           [[plain "One"], [plain "Two"], [plain "Three"]]
            , "Empty table" =:
              "\\begin{tabular}{}\\end{tabular}" =?>
              simpleTable' [] []
            , "Table with fixed column width" =:
              "\\begin{tabular}{|p{5cm}r|}One & Two\\\\ \\end{tabular}" =?>
              simpleTable' [AlignLeft,AlignRight] [[plain "One", plain "Two"]]
            , "Table with empty column separators" =:
              "\\begin{tabular}{@{}r@{}l}One & Two\\\\ \\end{tabular}" =?>
              simpleTable' [AlignRight,AlignLeft] [[plain "One", plain "Two"]]
            , "Table with custom column separators" =:
              T.unlines [ "\\begin{tabular}{@{($\\to$)}r@{\\hspace{2cm}}l}"
                      , "One&Two\\\\"
                      , "\\end{tabular}" ] =?>
              simpleTable' [AlignRight,AlignLeft] [[plain "One", plain "Two"]]
            , "Table with vertical alignment argument" =:
              "\\begin{tabular}[t]{r|r}One & Two\\\\ \\end{tabular}" =?>
              simpleTable' [AlignRight,AlignRight] [[plain "One", plain "Two"]]
            , "Table with multicolumn item" =:
              "\\begin{tabular}{l c r}\\multicolumn{2}{c}{One} & Two\\\\ \\end{tabular}" =?>
              table' [AlignLeft, AlignCenter, AlignRight]
                     [ Row nullAttr [ cell AlignCenter (RowSpan 1) (ColSpan 2) (plain "One")
                                    , simpleCell (plain "Two")
                                    ]
                     ]
            , "table with multicolumn item (#6596)" =:
              "\\begin{tabular}{l c r}One & \\multicolumn{2}{c}{Two} & \\\\ \\end{tabular}" =?>
              table' [AlignLeft, AlignCenter, AlignRight]
                     [ Row nullAttr [ simpleCell (plain "One")
                                    , cell AlignCenter (RowSpan 1) (ColSpan 2) (plain "Two")
                                    ]
                     ]
            , "Table with multirow item" =:
              T.unlines ["\\begin{tabular}{c}"
                        ,"\\multirow{2}{5em}{One}\\\\Two\\\\"
                        ,"\\end{tabular}"
                        ] =?>
              table' [AlignCenter]
                    [ Row nullAttr [ cell AlignDefault (RowSpan 2) (ColSpan 1) (plain "One") ]
                    , Row nullAttr [ simpleCell (plain "Two") ]
                    ]
            , "Table with multirow item using full prototype" =:
              T.unlines ["\\begin{tabular}{c}"
                        ,"\\multirow[c]{2}[3]{5em}[1in]{One}\\\\Two\\\\"
                        ,"\\end{tabular}"
                        ] =?>
              table' [AlignCenter]
                    [ Row nullAttr [ cell AlignDefault (RowSpan 2) (ColSpan 1) (plain "One") ]
                    , Row nullAttr [ simpleCell (plain "Two") ]
                    ]
            , "Table with nested multirow/multicolumn item" =:
              T.unlines [ "\\begin{tabular}{c c c c}"
                        , "\\multicolumn{3}{c}{\\multirow{2}{5em}{One}}&Two\\\\"
                        , "\\multicolumn{2}{c}{} & & Three\\\\"
                        , "Four&Five&Six&Seven\\\\"
                        , "\\end{tabular}"
                        ] =?>
              table' [AlignCenter, AlignCenter, AlignCenter, AlignCenter]
                     [ Row nullAttr [ cell AlignCenter (RowSpan 2) (ColSpan 3) (plain "One")
                                    , simpleCell (plain "Two")
                                    ]
                     , Row nullAttr [ simpleCell (plain "Three") ]
                     , Row nullAttr [ simpleCell (plain "Four")
                                    , simpleCell (plain "Five")
                                    , simpleCell (plain "Six")
                                    , simpleCell (plain "Seven")
                                    ]
                     ]
            , "Table with multicolumn header" =:
              T.unlines [ "\\begin{tabular}{ |l|l| }"
                        , "\\hline\\multicolumn{2}{|c|}{Header}\\\\"
                        , "\\hline key & val\\\\"
                        , "\\hline\\end{tabular}"
                        ] =?>
              table emptyCaption
                    (zip [AlignLeft, AlignLeft] (repeat ColWidthDefault))
                    (TableHead nullAttr [ Row nullAttr [cell AlignCenter (RowSpan 1) (ColSpan 2) (plain "Header")]])
                    [TableBody nullAttr 0 [] [Row nullAttr [ simpleCell (plain "key")
                                                           , simpleCell (plain "val")
                                                           ]
                                             ]
                    ]
                    (TableFoot nullAttr [])
            , "Table with normal empty cells" =:
              T.unlines [ "\\begin{tabular}{|r|r|r|}"
                        , "A &   & B \\\\"
                        , "  & C &"
                        , "\\end{tabular}"
                        ] =?>
              table emptyCaption
                    (replicate 3 (AlignRight, ColWidthDefault))
                    (TableHead nullAttr [])
                    [TableBody nullAttr 0 []
                      [Row nullAttr [ simpleCell (plain "A")
                                    , emptyCell
                                    , simpleCell (plain "B")
                                    ]
                      ,Row nullAttr [ emptyCell
                                    , simpleCell (plain "C")
                                    , emptyCell
                                    ]]]
                    (TableFoot nullAttr [])
            ]
  
          , testGroup "citations"
            [ natbibCitations
            , biblatexCitations
            ]
  
          , testGroup "images"
            [ "Basic image" =:
              "\\includegraphics{foo.png}" =?>
              para (image "foo.png" "" (text "image"))
            , "Basic image with blank options" =:
              "\\includegraphics[]{foo.png}" =?>
              para (image "foo.png" "" (text "image"))
            , "Image with both width and height" =:
              "\\includegraphics[width=17cm,height=5cm]{foo.png}" =?>
              para (imageWith ("", [], [("width", "17cm"), ("height", "5cm")]) "foo.png" "" "image")
            , "Image with width and height and a bunch of other options" =:
              "\\includegraphics[width=17cm,height=5cm,clip,keepaspectratio]{foo.png}" =?>
              para (imageWith ("", [], [("width", "17cm"), ("height", "5cm")]) "foo.png" "" "image")
            , "Image with just width" =:
              "\\includegraphics[width=17cm]{foo.png}" =?>
              para (imageWith ("", [], [("width", "17cm")]) "foo.png" "" "image")
            , "Image with just height" =:
              "\\includegraphics[height=17cm]{foo.png}" =?>
              para (imageWith ("", [], [("height", "17cm")]) "foo.png" "" "image")
            , "Image width relative to textsize" =:
              "\\includegraphics[width=0.6\\textwidth]{foo.png}" =?>
              para (imageWith ("", [], [("width", "60%")]) "foo.png" "" "image")
            , "Image with options with spaces" =:
              "\\includegraphics[width=12cm, height = 5cm]{foo.png}" =?>
              para (imageWith ("", [], [("width", "12cm"), ("height", "5cm")]) "foo.png" "" "image")
            ]
  
          , let hex = ['0'..'9']++['a'..'f'] in
            testGroup "Character Escapes"
            [ "Two-character escapes" =:
              mconcat ["^^" <> T.pack [i,j] | i <- hex, j <- hex] =?>
              para (str $ T.pack ['\0'..'\255'])
            , "One-character escapes" =:
              mconcat ["^^" <> T.pack [i] | i <- hex] =?>
              para (str $ T.pack $ ['p'..'y']++['!'..'&'])
            ]
          , testGroup "memoir scene breaks"
            [ "plainbreak" =:
              "hello\\plainbreak{2}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            , "plainbreak*" =:
              "hello\\plainbreak*{2}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            , "fancybreak" =:
              "hello\\fancybreak{b r e a k}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            , "fancybreak*" =:
              "hello\\fancybreak*{b r e a k}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            , "plainfancybreak" =:
              "hello\\plainfancybreak{4}{2}{b r e a k}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            , "plainfancybreak*" =:
              "hello\\plainfancybreak*{4}{2}{b r e a k}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            , "pfbreak" =:
              "hello\\pfbreak{}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            , "pfbreak*" =:
              "hello\\pfbreak*{}goodbye" =?>
              para (str "hello") <> horizontalRule <> para (str "goodbye")
            ]
          , testGroup "biblatex roman numerals"
            [ "upper" =:
              "number \\RN{12}" =?>
              para (str "number" <> space <> str "XII")
            , "lower" =:
              "number \\Rn{29}" =?>
              para (str "number" <> space <> str "xxix")
            , "leading zero" =:
              "\\Rn{014}" =?>
              para (str "xiv")
            , "surrounding spaces" =:
              "number \\Rn{ 41 }" =?>
              para (str "number" <> space <> str "xli")
            , "zero" =:
              "\\RN{0}" =?>
              para (str "")
            , "space then unbraced argument" =:
              "\\RN 7 ok" =?>
              para (str "VII" <> space <> str "ok")
            , "space before braced argument" =:
              "\\Rn {13}ok" =?>
              para (str "xiiiok")
            ]
          , testGroup "polyglossia language spans"
            [ "french" =:
              "hello \\textfrench{bonjour}" =?>
              para (str "hello" <> space <> spanWith ("", [], [("lang", "fr")]) (str "bonjour"))
            , "nested" =:
              "\\textfrench{quelle c'est \\textlatin{primus}?}" =?>
              para (spanWith ("", [], [("lang", "fr")]) $
                      str "quelle" <> space <> str "c\8217est" <> space <>
                      spanWith ("", [], [("lang", "la")]) (str "primus") <> str "?")
            , "with formatting" =:
              "\\textgerman{wie \\emph{spaet} ist es?}" =?>
              para (spanWith ("", [], [("lang", "de")]) $
                      str "wie" <> space <> emph (str "spaet") <> space <> str "ist" <> space <> str "es?")
            , "language options" =:
              "\\textgerman[variant=swiss]{hoechdeutsche}" =?>
              para (spanWith ("", [], [("lang", "de-CH")]) $ str "hoechdeutsche")
            , "unknown option fallback" =:
              "\\textgerman[variant=moon]{ueberhoechdeutsche}" =?>
              para (spanWith ("", [], [("lang", "de")]) $ str "ueberhoechdeutsche")
            ]
          ]
  
  baseCitation :: Citation
  baseCitation = Citation{ citationId      = "item1"
                         , citationPrefix  = []
                         , citationSuffix  = []
                         , citationMode    = AuthorInText
                         , citationNoteNum = 0
                         , citationHash    = 0
                         }
  
  rt :: String -> Inlines
  rt = rawInline "latex" . T.pack
  
  natbibCitations :: TestTree
  natbibCitations = testGroup "natbib"
    [ "citet" =: "\\citet{item1}"
      =?> para (cite [baseCitation] (rt "\\citet{item1}"))
    , "suffix" =: "\\citet[p.~30]{item1}"
      =?> para
          (cite [baseCitation{ citationSuffix = toList $ text "p.\160\&30" }] (rt "\\citet[p.~30]{item1}"))
    , "suffix long" =: "\\citet[p.~30, with suffix]{item1}"
      =?> para (cite [baseCitation{ citationSuffix =
                         toList $ text "p.\160\&30, with suffix" }] (rt "\\citet[p.~30, with suffix]{item1}"))
    , "multiple" =: "\\citeauthor{item1} \\citetext{\\citeyear{item1}; \\citeyear[p.~30]{item2}; \\citealp[see also][]{item3}}"
      =?> para (cite [baseCitation{ citationMode = AuthorInText }
                     ,baseCitation{ citationMode = SuppressAuthor
                                  , citationSuffix = [Str "p.\160\&30"]
                                  , citationId = "item2" }
                     ,baseCitation{ citationId = "item3"
                                  , citationPrefix = [Str "see",Space,Str "also"]
                                  , citationMode = NormalCitation }
                     ] (rt "\\citetext{\\citeyear{item1}; \\citeyear[p.~30]{item2}; \\citealp[see also][]{item3}}"))
    , "group" =: "\\citetext{\\citealp[see][p.~34--35]{item1}; \\citealp[also][chap. 3]{item3}}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationPrefix = [Str "see"]
                                  , citationSuffix = [Str "p.\160\&34\8211\&35"] }
                     ,baseCitation{ citationMode = NormalCitation
                                  , citationId = "item3"
                                  , citationPrefix = [Str "also"]
                                  , citationSuffix = [Str "chap.",Space,Str "3"] }
                     ] (rt "\\citetext{\\citealp[see][p.~34--35]{item1}; \\citealp[also][chap. 3]{item3}}"))
    , "suffix and locator" =: "\\citep[pp.~33, 35--37, and nowhere else]{item1}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationSuffix = [Str "pp.\160\&33,",Space,Str "35\8211\&37,",Space,Str "and",Space,Str "nowhere",Space, Str "else"] }] (rt "\\citep[pp.~33, 35--37, and nowhere else]{item1}"))
    , "suffix only" =: "\\citep[and nowhere else]{item1}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationSuffix = toList $ text "and nowhere else" }] (rt "\\citep[and nowhere else]{item1}"))
    , "no author" =: "\\citeyearpar{item1}, and now Doe with a locator \\citeyearpar[p.~44]{item2}"
      =?> para (cite [baseCitation{ citationMode = SuppressAuthor }] (rt "\\citeyearpar{item1}") <>
                text ", and now Doe with a locator " <>
                cite [baseCitation{ citationMode = SuppressAuthor
                                  , citationSuffix = [Str "p.\160\&44"]
                                  , citationId = "item2" }] (rt "\\citeyearpar[p.~44]{item2}"))
    , "markup" =: "\\citep[\\emph{see}][p. \\textbf{32}]{item1}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationPrefix = [Emph [Str "see"]]
                                  , citationSuffix = [Str "p.",Space,
                                      Strong [Str "32"]] }] (rt "\\citep[\\emph{see}][p. \\textbf{32}]{item1}"))
    ]
  
  biblatexCitations :: TestTree
  biblatexCitations = testGroup "biblatex"
    [ "textcite" =: "\\textcite{item1}"
      =?> para (cite [baseCitation] (rt "\\textcite{item1}"))
    , "suffix" =: "\\textcite[p.~30]{item1}"
      =?> para
          (cite [baseCitation{ citationSuffix = toList $ text "p.\160\&30" }] (rt "\\textcite[p.~30]{item1}"))
    , "suffix long" =: "\\textcite[p.~30, with suffix]{item1}"
      =?> para (cite [baseCitation{ citationSuffix =
                         toList $ text "p.\160\&30, with suffix" }] (rt "\\textcite[p.~30, with suffix]{item1}"))
    , "multiple" =: "\\textcites{item1}[p.~30]{item2}[see also][]{item3}"
      =?> para (cite [baseCitation{ citationMode = AuthorInText }
                     ,baseCitation{ citationMode = NormalCitation
                                  , citationSuffix = [Str "p.\160\&30"]
                                  , citationId = "item2" }
                     ,baseCitation{ citationId = "item3"
                                  , citationPrefix = [Str "see",Space,Str "also"]
                                  , citationMode = NormalCitation }
                     ] (rt "\\textcites{item1}[p.~30]{item2}[see also][]{item3}"))
    , "group" =: "\\autocites[see][p.~34--35]{item1}[also][chap. 3]{item3}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationPrefix = [Str "see"]
                                  , citationSuffix = [Str "p.\160\&34\8211\&35"] }
                     ,baseCitation{ citationMode = NormalCitation
                                  , citationId = "item3"
                                  , citationPrefix = [Str "also"]
                                  , citationSuffix = [Str "chap.",Space,Str "3"] }
                     ] (rt "\\autocites[see][p.~34--35]{item1}[also][chap. 3]{item3}"))
    , "suffix and locator" =: "\\autocite[pp.~33, 35--37, and nowhere else]{item1}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationSuffix = [Str "pp.\160\&33,",Space,Str "35\8211\&37,",Space,Str "and",Space,Str "nowhere",Space, Str "else"] }] (rt "\\autocite[pp.~33, 35--37, and nowhere else]{item1}"))
    , "suffix only" =: "\\autocite[and nowhere else]{item1}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationSuffix = toList $ text "and nowhere else" }] (rt "\\autocite[and nowhere else]{item1}"))
    , "no author" =: "\\autocite*{item1}, and now Doe with a locator \\autocite*[p.~44]{item2}"
      =?> para (cite [baseCitation{ citationMode = SuppressAuthor }] (rt "\\autocite*{item1}") <>
                text ", and now Doe with a locator " <>
                cite [baseCitation{ citationMode = SuppressAuthor
                                  , citationSuffix = [Str "p.\160\&44"]
                                  , citationId = "item2" }] (rt "\\autocite*[p.~44]{item2}"))
    , "markup" =: "\\autocite[\\emph{see}][p. \\textbf{32}]{item1}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation
                                  , citationPrefix = [Emph [Str "see"]]
                                  , citationSuffix = [Str "p.",Space,
                                      Strong [Str "32"]] }] (rt "\\autocite[\\emph{see}][p. \\textbf{32}]{item1}"))
    , "parencite" =: "\\parencite{item1}"
      =?> para (cite [baseCitation{ citationMode = NormalCitation }] (rt "\\parencite{item1}"))
    ]
  