{-# LANGUAGE OverloadedStrings #-} {- | Module : Tests.Readers.LaTeX Copyright : © 2006-2024 John MacFarlane License : GNU GPL, version 2 or above Maintainer : John MacFarlane 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}")) ]