@@ -0,0 +1,1262 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Tests.Readers.Muse (tests) where
3+
4+import Data.List (intersperse)
5+import Data.Text (Text)
6+import qualified Data.Text as T
7+import Test.Tasty
8+import Test.Tasty.QuickCheck
9+import Tests.Helpers
10+import Text.Pandoc
11+import Text.Pandoc.Arbitrary ()
12+import Text.Pandoc.Builder
13+import Text.Pandoc.Shared (underlineSpan)
14+import Text.Pandoc.Walk (walk)
15+
16+amuse :: Text -> Pandoc
17+amuse = purely $ readMuse def { readerExtensions = extensionsFromList [Ext_amuse]}
18+
19+emacsMuse :: Text -> Pandoc
20+emacsMuse = purely $ readMuse def { readerExtensions = emptyExtensions }
21+
22+infix 4 =:
23+(=:) :: ToString c
24+ => String -> (Text, c) -> TestTree
25+(=:) = test amuse
26+
27+spcSep :: [Inlines] -> Inlines
28+spcSep = mconcat . intersperse space
29+
30+-- Tables don't round-trip yet
31+--
32+makeRoundTrip :: Block -> Block
33+makeRoundTrip Table{} = Para [Str "table was here"]
34+makeRoundTrip (OrderedList (start, LowerAlpha, _) items) = OrderedList (start, Decimal, Period) items
35+makeRoundTrip (OrderedList (start, UpperAlpha, _) items) = OrderedList (start, Decimal, Period) items
36+makeRoundTrip x = x
37+
38+-- Demand that any AST produced by Muse reader and written by Muse writer can be read back exactly the same way.
39+-- Currently we remove tables and compare first rewrite to the second.
40+roundTrip :: Block -> Bool
41+roundTrip b = d' == d''
42+ where d = walk makeRoundTrip $ Pandoc nullMeta [b]
43+ d' = rewrite d
44+ d'' = rewrite d'
45+ rewrite = amuse . T.pack . (++ "\n") . T.unpack .
46+ purely (writeMuse def { writerExtensions = extensionsFromList [Ext_amuse]
47+ , writerWrapText = WrapPreserve
48+ })
49+
50+tests :: [TestTree]
51+tests =
52+ [ testGroup "Inlines"
53+ [ "Plain String" =:
54+ "Hello, World" =?>
55+ para "Hello, World"
56+
57+ , "Muse is not XML" =: "<" =?> para "<"
58+
59+ , "Emphasis" =:
60+ "*Foo bar*" =?>
61+ para (emph . spcSep $ ["Foo", "bar"])
62+
63+ , "Comma after closing *" =:
64+ "Foo *bar*, baz" =?>
65+ para ("Foo " <> emph "bar" <> ", baz")
66+
67+ , "Letter after closing *" =:
68+ "Foo *bar*x baz" =?>
69+ para "Foo *bar*x baz"
70+
71+ , "Letter before opening *" =:
72+ "Foo x*bar* baz" =?>
73+ para "Foo x*bar* baz"
74+
75+ , "Emphasis tag" =:
76+ "<em>Foo bar</em>" =?>
77+ para (emph . spcSep $ ["Foo", "bar"])
78+
79+ , "Strong" =:
80+ "**Cider**" =?>
81+ para (strong "Cider")
82+
83+ , "Strong tag" =: "<strong>Strong</strong>" =?> para (strong "Strong")
84+
85+ , "Strong Emphasis" =:
86+ "***strength***" =?>
87+ para (strong . emph $ "strength")
88+
89+ , test emacsMuse "Underline"
90+ ("_Underline_" =?> para (underlineSpan "Underline"))
91+
92+ , "Superscript tag" =: "<sup>Superscript</sup>" =?> para (superscript "Superscript")
93+
94+ , "Subscript tag" =: "<sub>Subscript</sub>" =?> para (subscript "Subscript")
95+
96+ , "Strikeout tag" =: "<del>Strikeout</del>" =?> para (strikeout "Strikeout")
97+
98+ , "Opening inline tags" =: "foo <em> bar <strong>baz" =?> para "foo <em> bar <strong>baz"
99+
100+ , "Closing inline tags" =: "foo </em> bar </strong>baz" =?> para "foo </em> bar </strong>baz"
101+
102+ , "Tag soup" =: "foo <em> bar </strong>baz" =?> para "foo <em> bar </strong>baz"
103+
104+ -- Both inline tags must be within the same paragraph
105+ , "No multiparagraph inline tags" =:
106+ T.unlines [ "First line"
107+ , "<em>Second line"
108+ , ""
109+ , "Fourth line</em>"
110+ ] =?>
111+ para "First line\n<em>Second line" <>
112+ para "Fourth line</em>"
113+
114+ , "Linebreak" =: "Line <br> break" =?> para ("Line" <> linebreak <> "break")
115+
116+ , "Trailing whitespace inside paragraph" =:
117+ T.unlines [ "First line " -- trailing whitespace here
118+ , "second line"
119+ ]
120+ =?> para "First line\nsecond line"
121+
122+ , "Non-breaking space" =: "Foo~~bar" =?> para "Foo\160bar"
123+ , "Single ~" =: "Foo~bar" =?> para "Foo~bar"
124+
125+ , testGroup "Code markup"
126+ [ "Code" =: "=foo(bar)=" =?> para (code "foo(bar)")
127+
128+ , "Not code" =: "a=b= =c=d" =?> para (text "a=b= =c=d")
129+
130+ -- Emacs Muse 3.20 parses this as code, we follow Amusewiki
131+ , "Not code if closing = is detached" =: "=this is not a code =" =?> para "=this is not a code ="
132+
133+ , "Not code if opening = is detached" =: "= this is not a code=" =?> para "= this is not a code="
134+
135+ , "Code if followed by comma" =:
136+ "Foo =bar=, baz" =?>
137+ para (text "Foo " <> code "bar" <> text ", baz")
138+
139+ , "One character code" =: "=c=" =?> para (code "c")
140+
141+ , "Three = characters is not a code" =: "===" =?> para "==="
142+
143+ , "Multiline code markup" =:
144+ "foo =bar\nbaz= end of code" =?>
145+ para (text "foo " <> code "bar\nbaz" <> text " end of code")
146+
147+{- Emacs Muse 3.20 has a bug: it publishes
148+ - <p>foo <code>bar
149+ -
150+ - baz</code> foo</p>
151+ - which is displayed as one paragraph by browsers.
152+ - We follow Amusewiki here and avoid joining paragraphs.
153+ -}
154+ , "No multiparagraph code" =:
155+ T.unlines [ "foo =bar"
156+ , ""
157+ , "baz= foo"
158+ ] =?>
159+ para "foo =bar" <>
160+ para "baz= foo"
161+
162+ , "Code at the beginning of paragraph but not first column" =:
163+ " - =foo=" =?> bulletList [ para $ code "foo" ]
164+ ]
165+
166+ , "Code tag" =: "<code>foo(bar)</code>" =?> para (code "foo(bar)")
167+
168+ , "Verbatim tag" =: "*<verbatim>*</verbatim>*" =?> para (emph "*")
169+
170+ , "Verbatim inside code" =: "<code><verbatim>foo</verbatim></code>" =?> para (code "<verbatim>foo</verbatim>")
171+
172+ , "Verbatim tag after text" =: "Foo <verbatim>bar</verbatim>" =?> para "Foo bar"
173+
174+ , "Class tag" =: "<class name=\"foo\">bar</class>" =?> para (spanWith ("", ["foo"], []) "bar")
175+ , "Class tag without name" =: "<class>foobar</class>" =?> para (spanWith ("", [], []) "foobar")
176+
177+ -- <em> tag should match with the last </em> tag, not verbatim one
178+ , "Nested \"</em>\" inside em tag" =: "<em>foo<verbatim></em></verbatim>bar</em>" =?> para (emph "foo</em>bar")
179+
180+ , testGroup "Links"
181+ [ "Link without description" =:
182+ "[[https://amusewiki.org/]]" =?>
183+ para (link "https://amusewiki.org/" "" (str "https://amusewiki.org/"))
184+ , "Link with description" =:
185+ "[[https://amusewiki.org/][A Muse Wiki]]" =?>
186+ para (link "https://amusewiki.org/" "" (text "A Muse Wiki"))
187+ , "Image" =:
188+ "[[image.jpg]]" =?>
189+ para (image "image.jpg" "" mempty)
190+ , "Image with description" =:
191+ "[[image.jpg][Image]]" =?>
192+ para (image "image.jpg" "" (text "Image"))
193+ , "Image link" =:
194+ "[[URL:image.jpg]]" =?>
195+ para (link "image.jpg" "" (str "image.jpg"))
196+ , "Image link with description" =:
197+ "[[URL:image.jpg][Image]]" =?>
198+ para (link "image.jpg" "" (text "Image"))
199+ -- Implicit links are supported in Emacs Muse, but not in Amusewiki:
200+ -- https://github.com/melmothx/text-amuse/issues/18
201+ --
202+ -- This test also makes sure '=' without whitespace is not treated as code markup
203+ , "No implicit links" =: "http://example.org/index.php?action=view&id=1"
204+ =?> para "http://example.org/index.php?action=view&id=1"
205+ ]
206+
207+ , testGroup "Literal"
208+ [ test emacsMuse "Inline literal"
209+ ("Foo<literal style=\"html\">lit</literal>bar" =?>
210+ para (text "Foo" <> rawInline "html" "lit" <> text "bar"))
211+ ]
212+ ]
213+
214+ , testGroup "Blocks"
215+ [ testProperty "Round trip" roundTrip,
216+ "Block elements end paragraphs" =:
217+ T.unlines [ "First paragraph"
218+ , "----"
219+ , "Second paragraph"
220+ ] =?> para (text "First paragraph") <> horizontalRule <> para (text "Second paragraph")
221+ , testGroup "Horizontal rule"
222+ [ "Less than 4 dashes is not a horizontal rule" =: "---" =?> para (text "---")
223+ , "4 dashes is a horizontal rule" =: "----" =?> horizontalRule
224+ , "5 dashes is a horizontal rule" =: "-----" =?> horizontalRule
225+ , "4 dashes with spaces is a horizontal rule" =: "---- " =?> horizontalRule
226+ ]
227+ , testGroup "Paragraphs"
228+ [ "Simple paragraph" =:
229+ T.unlines [ "First line"
230+ , "second line."
231+ ] =?>
232+ para "First line\nsecond line."
233+ , "Indented paragraph" =:
234+ T.unlines [ " First line"
235+ , "second line."
236+ ] =?>
237+ para "First line\nsecond line."
238+ -- Emacs Muse starts a blockquote on the second line.
239+ -- We copy Amusewiki behavior and require a blank line to start a blockquote.
240+ , "Indentation in the middle of paragraph" =:
241+ T.unlines [ "First line"
242+ , " second line"
243+ , "third line"
244+ ] =?>
245+ para "First line\nsecond line\nthird line"
246+ , "Quote" =:
247+ " This is a quotation\n" =?>
248+ blockQuote (para "This is a quotation")
249+ , "Indentation does not indicate quote inside quote tag" =:
250+ T.unlines [ "<quote>"
251+ , " Not a nested quote"
252+ , "</quote>"
253+ ] =?>
254+ blockQuote (para "Not a nested quote")
255+ , "Multiline quote" =:
256+ T.unlines [ " This is a quotation"
257+ , " with a continuation"
258+ ] =?>
259+ blockQuote (para "This is a quotation\nwith a continuation")
260+ , testGroup "Div"
261+ [ "Div without id" =:
262+ T.unlines [ "<div>"
263+ , "Foo bar"
264+ , "</div>"
265+ ] =?>
266+ divWith nullAttr (para "Foo bar")
267+ , "Div with id" =:
268+ T.unlines [ "<div id=\"foo\">"
269+ , "Foo bar"
270+ , "</div>"
271+ ] =?>
272+ divWith ("foo", [], []) (para "Foo bar")
273+ ]
274+ , "Verse" =:
275+ T.unlines [ "> This is"
276+ , "> First stanza"
277+ , ">" -- Emacs produces verbatim ">" here, we follow Amusewiki
278+ , "> And this is"
279+ , "> Second stanza"
280+ , ">"
281+ , ""
282+ , ">"
283+ , ""
284+ , "> Another verse"
285+ , "> is here"
286+ ] =?>
287+ lineBlock [ "This is"
288+ , "First stanza"
289+ , ""
290+ , "And this is"
291+ , "\160\160Second stanza"
292+ , ""
293+ ] <>
294+ lineBlock [ "" ] <>
295+ lineBlock [ "Another verse"
296+ , "\160\160\160is here"
297+ ]
298+ ]
299+ , "Verse in list" =: " - > foo" =?> bulletList [ lineBlock [ "foo" ] ]
300+ , "Verse line starting with emphasis" =: "> *foo* bar" =?> lineBlock [ emph "foo" <> text " bar" ]
301+ , "Multiline verse in list" =:
302+ T.unlines [ " - > foo"
303+ , " > bar"
304+ ] =?>
305+ bulletList [ lineBlock [ "foo", "bar" ] ]
306+ , "Paragraph after verse in list" =:
307+ T.unlines [ " - > foo"
308+ , " bar"
309+ ] =?>
310+ bulletList [ lineBlock [ "foo" ] <> para "bar" ]
311+ , "Empty quote tag" =:
312+ T.unlines [ "<quote>"
313+ , "</quote>"
314+ ]
315+ =?> blockQuote mempty
316+ , "Quote tag" =:
317+ T.unlines [ "<quote>"
318+ , "Hello, world"
319+ , "</quote>"
320+ ]
321+ =?> blockQuote (para $ text "Hello, world")
322+ , "Nested quote tag" =:
323+ T.unlines [ "<quote>"
324+ , "foo"
325+ , "<quote>"
326+ , "bar"
327+ , "</quote>"
328+ , "baz"
329+ , "</quote>"
330+ ] =?>
331+ blockQuote (para "foo" <> blockQuote (para "bar") <> para "baz")
332+ , "Indented quote inside list" =:
333+ T.unlines [ " - <quote>"
334+ , " foo"
335+ , " </quote>"
336+ ] =?>
337+ bulletList [ blockQuote (para "foo") ]
338+ , "Verse tag" =:
339+ T.unlines [ "<verse>"
340+ , ""
341+ , "Foo bar baz"
342+ , " One two three"
343+ , ""
344+ , "</verse>"
345+ ] =?>
346+ lineBlock [ ""
347+ , text "Foo bar baz"
348+ , text "\160\160One two three"
349+ , ""
350+ ]
351+ , "Verse tag with empty line inside" =:
352+ T.unlines [ "<verse>"
353+ , ""
354+ , "</verse>"
355+ ] =?>
356+ lineBlock [ "" ]
357+ , testGroup "Example"
358+ [ "Braces on separate lines" =:
359+ T.unlines [ "{{{"
360+ , "Example line"
361+ , "}}}"
362+ ] =?>
363+ codeBlock "Example line"
364+ , "Spaces after opening braces" =:
365+ T.unlines [ "{{{ "
366+ , "Example line"
367+ , "}}}"
368+ ] =?>
369+ codeBlock "Example line"
370+ , "One blank line in the beginning" =:
371+ T.unlines [ "{{{"
372+ , ""
373+ , "Example line"
374+ , "}}}"
375+ ] =?>
376+ codeBlock "\nExample line"
377+ , "One blank line in the end" =:
378+ T.unlines [ "{{{"
379+ , "Example line"
380+ , ""
381+ , "}}}"
382+ ] =?>
383+ codeBlock "Example line\n"
384+ -- Amusewiki requires braces to be on separate line,
385+ -- this is an extension.
386+ , "One line" =:
387+ "{{{Example line}}}" =?>
388+ codeBlock "Example line"
389+ ]
390+ , testGroup "Example tag"
391+ [ "Tags on separate lines" =:
392+ T.unlines [ "<example>"
393+ , "Example line"
394+ , "</example>"
395+ ] =?>
396+ codeBlock "Example line"
397+ , "One line" =:
398+ "<example>Example line</example>" =?>
399+ codeBlock "Example line"
400+ , "One blank line in the beginning" =:
401+ T.unlines [ "<example>"
402+ , ""
403+ , "Example line"
404+ , "</example>"
405+ ] =?>
406+ codeBlock "\nExample line"
407+ , "One blank line in the end" =:
408+ T.unlines [ "<example>"
409+ , "Example line"
410+ , ""
411+ , "</example>"
412+ ] =?>
413+ codeBlock "Example line\n"
414+ , "Example inside list" =:
415+ T.unlines [ " - <example>"
416+ , " foo"
417+ , " </example>"
418+ ] =?>
419+ bulletList [ codeBlock "foo" ]
420+ , "Empty example inside list" =:
421+ T.unlines [ " - <example>"
422+ , " </example>"
423+ ] =?>
424+ bulletList [ codeBlock "" ]
425+ , "Example inside list with empty lines" =:
426+ T.unlines [ " - <example>"
427+ , " foo"
428+ , " </example>"
429+ , ""
430+ , " bar"
431+ , ""
432+ , " <example>"
433+ , " baz"
434+ , " </example>"
435+ ] =?>
436+ bulletList [ codeBlock "foo" <> para "bar" <> codeBlock "baz" ]
437+ , "Indented example inside list" =:
438+ T.unlines [ " - <example>"
439+ , " foo"
440+ , " </example>"
441+ ] =?>
442+ bulletList [ codeBlock "foo" ]
443+ , "Example inside definition list" =:
444+ T.unlines [ " foo :: <example>"
445+ , " bar"
446+ , " </example>"
447+ ] =?>
448+ definitionList [ ("foo", [codeBlock "bar"]) ]
449+ , "Example inside list definition with empty lines" =:
450+ T.unlines [ " term :: <example>"
451+ , " foo"
452+ , " </example>"
453+ , ""
454+ , " bar"
455+ , ""
456+ , " <example>"
457+ , " baz"
458+ , " </example>"
459+ ] =?>
460+ definitionList [ ("term", [codeBlock "foo" <> para "bar" <> codeBlock "baz"]) ]
461+ , "Example inside note" =:
462+ T.unlines [ "Foo[1]"
463+ , ""
464+ , "[1] <example>"
465+ , " bar"
466+ , " </example>"
467+ ] =?>
468+ para ("Foo" <> note (codeBlock "bar"))
469+ ]
470+ , testGroup "Literal blocks"
471+ [ test emacsMuse "Literal block"
472+ (T.unlines [ "<literal style=\"latex\">"
473+ , "\\newpage"
474+ , "</literal>"
475+ ] =?>
476+ rawBlock "latex" "\\newpage")
477+ ]
478+ , "Center" =:
479+ T.unlines [ "<center>"
480+ , "Hello, world"
481+ , "</center>"
482+ ] =?>
483+ para (text "Hello, world")
484+ , "Right" =:
485+ T.unlines [ "<right>"
486+ , "Hello, world"
487+ , "</right>"
488+ ] =?>
489+ para (text "Hello, world")
490+ , testGroup "Comments"
491+ [ "Comment tag" =: "<comment>\nThis is a comment\n</comment>" =?> (mempty::Blocks)
492+ , "Line comment" =: "; Comment" =?> (mempty::Blocks)
493+ , "Empty comment" =: ";" =?> (mempty::Blocks)
494+ , "Text after empty comment" =: ";\nfoo" =?> para "foo" -- Make sure we don't consume newline while looking for whitespace
495+ , "Not a comment (does not start with a semicolon)" =: " ; Not a comment" =?> para (text "; Not a comment")
496+ , "Not a comment (has no space after semicolon)" =: ";Not a comment" =?> para (text ";Not a comment")
497+ ]
498+ , testGroup "Headers"
499+ [ "Part" =:
500+ "* First level" =?>
501+ header 1 "First level"
502+ , "Chapter" =:
503+ "** Second level" =?>
504+ header 2 "Second level"
505+ , "Section" =:
506+ "*** Third level" =?>
507+ header 3 "Third level"
508+ , "Subsection" =:
509+ "**** Fourth level" =?>
510+ header 4 "Fourth level"
511+ , "Subsubsection" =:
512+ "***** Fifth level" =?>
513+ header 5 "Fifth level"
514+ , "Whitespace is required after *" =: "**Not a header" =?> para "**Not a header"
515+ , "No headers in footnotes" =:
516+ T.unlines [ "Foo[1]"
517+ , "[1] * Bar"
518+ ] =?>
519+ para (text "Foo" <>
520+ note (para "* Bar"))
521+ , "No headers in quotes" =:
522+ T.unlines [ "<quote>"
523+ , "* Hi"
524+ , "</quote>"
525+ ] =?>
526+ blockQuote (para "* Hi")
527+ , "Headers consume anchors" =:
528+ T.unlines [ "** Foo"
529+ , "#bar"
530+ ] =?>
531+ headerWith ("bar",[],[]) 2 "Foo"
532+ , "Headers don't consume anchors separated with a blankline" =:
533+ T.unlines [ "** Foo"
534+ , ""
535+ , "#bar"
536+ ] =?>
537+ header 2 "Foo" <>
538+ para (spanWith ("bar", [], []) mempty)
539+ , "Headers terminate lists" =:
540+ T.unlines [ " - foo"
541+ , "* bar"
542+ ] =?>
543+ bulletList [ para "foo" ] <>
544+ header 1 "bar"
545+ ]
546+ , testGroup "Directives"
547+ [ "Title" =:
548+ "#title Document title" =?>
549+ let titleInline = toList "Document title"
550+ meta = setMeta "title" (MetaInlines titleInline) nullMeta
551+ in Pandoc meta mempty
552+ -- Emacs Muse documentation says that "You can use any combination
553+ -- of uppercase and lowercase letters for directives",
554+ -- but also allows '-', which is not documented, but used for disable-tables.
555+ , test emacsMuse "Disable tables"
556+ ("#disable-tables t" =?>
557+ Pandoc (setMeta "disable-tables" (MetaInlines $ toList "t") nullMeta) mempty)
558+ , "Multiple directives" =:
559+ T.unlines [ "#title Document title"
560+ , "#subtitle Document subtitle"
561+ ] =?>
562+ Pandoc (setMeta "title" (MetaInlines $ toList "Document title") $
563+ setMeta "subtitle" (MetaInlines $ toList "Document subtitle") nullMeta) mempty
564+ , "Multiline directive" =:
565+ T.unlines [ "#title Document title"
566+ , "#notes First line"
567+ , "and second line"
568+ , "#author Name"
569+ ] =?>
570+ Pandoc (setMeta "title" (MetaInlines $ toList "Document title") $
571+ setMeta "notes" (MetaInlines $ toList "First line\nand second line") $
572+ setMeta "author" (MetaInlines $ toList "Name") nullMeta) mempty
573+ ]
574+ , testGroup "Anchors"
575+ [ "Anchor" =:
576+ T.unlines [ "; A comment to make sure anchor is not parsed as a directive"
577+ , "#anchor Target"
578+ ] =?>
579+ para (spanWith ("anchor", [], []) mempty <> "Target")
580+ , "Anchor cannot start with a number" =:
581+ T.unlines [ "; A comment to make sure anchor is not parsed as a directive"
582+ , "#0notanchor Target"
583+ ] =?>
584+ para "#0notanchor Target"
585+ , "Not anchor if starts with a space" =:
586+ " #notanchor Target" =?>
587+ para "#notanchor Target"
588+ , "Anchor inside a paragraph" =:
589+ T.unlines [ "Paragraph starts here"
590+ , "#anchor and ends here."
591+ ] =?>
592+ para ("Paragraph starts here\n" <> spanWith ("anchor", [], []) mempty <> "and ends here.")
593+ ]
594+ , testGroup "Footnotes"
595+ [ "Simple footnote" =:
596+ T.unlines [ "Here is a footnote[1]."
597+ , ""
598+ , "[1] Footnote contents"
599+ ] =?>
600+ para (text "Here is a footnote" <>
601+ note (para "Footnote contents") <>
602+ str ".")
603+ , "Recursive footnote" =:
604+ T.unlines [ "Start recursion here[1]"
605+ , ""
606+ , "[1] Recursion continues here[1]"
607+ ] =?>
608+ para (text "Start recursion here" <>
609+ note (para "Recursion continues here[1]"))
610+ , "No zero footnotes" =:
611+ T.unlines [ "Here is a footnote[0]."
612+ , ""
613+ , "[0] Footnote contents"
614+ ] =?>
615+ para "Here is a footnote[0]." <>
616+ para "[0] Footnote contents"
617+ , "Footnotes can't start with zero" =:
618+ T.unlines [ "Here is a footnote[01]."
619+ , ""
620+ , "[01] Footnote contents"
621+ ] =?>
622+ para "Here is a footnote[01]." <>
623+ para "[01] Footnote contents"
624+ , testGroup "Multiparagraph footnotes"
625+ [ "Amusewiki multiparagraph footnotes" =:
626+ T.unlines [ "Multiparagraph[1] footnotes[2]"
627+ , ""
628+ , "[1] First footnote paragraph"
629+ , ""
630+ , " Second footnote paragraph"
631+ , "with continuation"
632+ , ""
633+ , "Not a note"
634+ , "[2] Second footnote"
635+ ] =?>
636+ para (text "Multiparagraph" <>
637+ note (para "First footnote paragraph" <>
638+ para "Second footnote paragraph\nwith continuation") <>
639+ text " footnotes" <>
640+ note (para "Second footnote")) <>
641+ para (text "Not a note")
642+
643+ -- Verse requires precise indentation, so it is good to test indentation requirements
644+ , "Note continuation with verse" =:
645+ T.unlines [ "Foo[1]"
646+ , ""
647+ , "[1] Bar"
648+ , ""
649+ , " > Baz"
650+ ] =?>
651+ para ("Foo" <> note (para "Bar" <> lineBlock ["Baz"]))
652+ , test emacsMuse "Emacs multiparagraph footnotes"
653+ (T.unlines
654+ [ "First footnote reference[1] and second footnote reference[2]."
655+ , ""
656+ , "[1] First footnote paragraph"
657+ , ""
658+ , "Second footnote"
659+ , "paragraph"
660+ , ""
661+ , "[2] Third footnote paragraph"
662+ , ""
663+ , "Fourth footnote paragraph"
664+ ] =?>
665+ para (text "First footnote reference" <>
666+ note (para "First footnote paragraph" <>
667+ para "Second footnote\nparagraph") <>
668+ text " and second footnote reference" <>
669+ note (para "Third footnote paragraph" <>
670+ para "Fourth footnote paragraph") <>
671+ text "."))
672+ ]
673+ ]
674+ ]
675+ , testGroup "Tables"
676+ [ "Two cell table" =:
677+ "One | Two" =?>
678+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
679+ []
680+ [[plain "One", plain "Two"]]
681+ , "Table with multiple words" =:
682+ "One two | three four" =?>
683+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
684+ []
685+ [[plain "One two", plain "three four"]]
686+ , "Not a table" =:
687+ "One| Two" =?>
688+ para (text "One| Two")
689+ , "Not a table again" =:
690+ "One |Two" =?>
691+ para (text "One |Two")
692+ , "Two line table" =:
693+ T.unlines
694+ [ "One | Two"
695+ , "Three | Four"
696+ ] =?>
697+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
698+ []
699+ [[plain "One", plain "Two"],
700+ [plain "Three", plain "Four"]]
701+ , "Table with one header" =:
702+ T.unlines
703+ [ "First || Second"
704+ , "Third | Fourth"
705+ ] =?>
706+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
707+ [plain "First", plain "Second"]
708+ [[plain "Third", plain "Fourth"]]
709+ , "Table with two headers" =:
710+ T.unlines
711+ [ "First || header"
712+ , "Second || header"
713+ , "Foo | bar"
714+ ] =?>
715+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
716+ [plain "First", plain "header"]
717+ [[plain "Second", plain "header"],
718+ [plain "Foo", plain "bar"]]
719+ , "Header and footer reordering" =:
720+ T.unlines
721+ [ "Foo ||| bar"
722+ , "Baz || foo"
723+ , "Bar | baz"
724+ ] =?>
725+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
726+ [plain "Baz", plain "foo"]
727+ [[plain "Bar", plain "baz"],
728+ [plain "Foo", plain "bar"]]
729+ , "Table with caption" =:
730+ T.unlines
731+ [ "Foo || bar || baz"
732+ , "First | row | here"
733+ , "Second | row | there"
734+ , "|+ Table caption +|"
735+ ] =?>
736+ table (text "Table caption") (replicate 3 (AlignDefault, 0.0))
737+ [plain "Foo", plain "bar", plain "baz"]
738+ [[plain "First", plain "row", plain "here"],
739+ [plain "Second", plain "row", plain "there"]]
740+ , "Caption without table" =:
741+ "|+ Foo bar baz +|" =?>
742+ table (text "Foo bar baz") [] [] []
743+ , "Table indented with space" =:
744+ T.unlines
745+ [ " Foo | bar"
746+ , " Baz | foo"
747+ , " Bar | baz"
748+ ] =?>
749+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
750+ []
751+ [[plain "Foo", plain "bar"],
752+ [plain "Baz", plain "foo"],
753+ [plain "Bar", plain "baz"]]
754+ , "Empty cells" =:
755+ T.unlines
756+ [ " | Foo"
757+ , " |"
758+ , " bar |"
759+ , " || baz"
760+ ] =?>
761+ table mempty [(AlignDefault, 0.0), (AlignDefault, 0.0)]
762+ [plain "", plain "baz"]
763+ [[plain "", plain "Foo"],
764+ [plain "", plain ""],
765+ [plain "bar", plain ""]]
766+ ]
767+ , testGroup "Lists"
768+ [ "Bullet list" =:
769+ T.unlines
770+ [ " - Item1"
771+ , ""
772+ , " - Item2"
773+ ] =?>
774+ bulletList [ para "Item1"
775+ , para "Item2"
776+ ]
777+ , "Ordered list" =:
778+ T.unlines
779+ [ " 1. Item1"
780+ , ""
781+ , " 2. Item2"
782+ ] =?>
783+ orderedListWith (1, Decimal, Period) [ para "Item1"
784+ , para "Item2"
785+ ]
786+ , "Ordered list with implicit numbers" =:
787+ T.unlines
788+ [ " 1. Item1"
789+ , ""
790+ , " 1. Item2"
791+ , ""
792+ , " 1. Item3"
793+ ] =?>
794+ orderedListWith (1, Decimal, Period) [ para "Item1"
795+ , para "Item2"
796+ , para "Item3"
797+ ]
798+ , "Ordered list with roman numerals" =:
799+ T.unlines
800+ [ " i. First"
801+ , " ii. Second"
802+ , " iii. Third"
803+ , " iv. Fourth"
804+ ] =?>
805+ orderedListWith (1, LowerRoman, Period) [ para "First"
806+ , para "Second"
807+ , para "Third"
808+ , para "Fourth"
809+ ]
810+ , "Bullet list with empty items" =:
811+ T.unlines
812+ [ " -"
813+ , ""
814+ , " - Item2"
815+ ] =?>
816+ bulletList [ mempty
817+ , para "Item2"
818+ ]
819+ , "Ordered list with empty items" =:
820+ T.unlines
821+ [ " 1."
822+ , ""
823+ , " 2."
824+ , ""
825+ , " 3. Item3"
826+ ] =?>
827+ orderedListWith (1, Decimal, Period) [ mempty
828+ , mempty
829+ , para "Item3"
830+ ]
831+ , "Bullet list with last item empty" =:
832+ T.unlines
833+ [ " -"
834+ , ""
835+ , "foo"
836+ ] =?>
837+ bulletList [ mempty ] <>
838+ para "foo"
839+ , testGroup "Nested lists"
840+ [ "Nested bullet list" =:
841+ T.unlines [ " - Item1"
842+ , " - Item2"
843+ , " - Item3"
844+ , " - Item4"
845+ , " - Item5"
846+ , " - Item6"
847+ ] =?>
848+ bulletList [ para "Item1" <>
849+ bulletList [ para "Item2" <>
850+ bulletList [ para "Item3" ]
851+ , para "Item4" <>
852+ bulletList [ para "Item5" ]
853+ ]
854+ , para "Item6"
855+ ]
856+ , "Nested ordered list" =:
857+ T.unlines [ " 1. Item1"
858+ , " 1. Item2"
859+ , " 1. Item3"
860+ , " 2. Item4"
861+ , " 1. Item5"
862+ , " 2. Item6"
863+ ] =?>
864+ orderedListWith (1, Decimal, Period) [ para "Item1" <>
865+ orderedListWith (1, Decimal, Period) [ para "Item2" <>
866+ orderedListWith (1, Decimal, Period) [ para "Item3" ]
867+ , para "Item4" <>
868+ orderedListWith (1, Decimal, Period) [ para "Item5" ]
869+ ]
870+ , para "Item6"
871+ ]
872+ , "Mixed nested list" =:
873+ T.unlines
874+ [ " - Item1"
875+ , " - Item2"
876+ , " - Item3"
877+ , " - Item4"
878+ , " 1. Nested"
879+ , " 2. Ordered"
880+ , " 3. List"
881+ ] =?>
882+ bulletList [ mconcat [ para "Item1"
883+ , bulletList [ para "Item2"
884+ , para "Item3"
885+ ]
886+ ]
887+ , mconcat [ para "Item4"
888+ , orderedListWith (1, Decimal, Period) [ para "Nested"
889+ , para "Ordered"
890+ , para "List"
891+ ]
892+ ]
893+ ]
894+ , "Text::Amuse includes only one space in list marker" =:
895+ T.unlines
896+ [ " - First item"
897+ , " - Nested item"
898+ ] =?>
899+ bulletList [ para "First item" <> bulletList [ para "Nested item"]]
900+ ]
901+ , "List continuation" =:
902+ T.unlines
903+ [ " - a"
904+ , ""
905+ , " b"
906+ , ""
907+ , " c"
908+ ] =?>
909+ bulletList [ mconcat [ para "a"
910+ , para "b"
911+ , para "c"
912+ ]
913+ ]
914+ , "List continuation afeter nested list" =:
915+ T.unlines
916+ [ " - - foo"
917+ , ""
918+ , " bar"
919+ ] =?>
920+ bulletList [ bulletList [ para "foo" ] <>
921+ para "bar"
922+ ]
923+ -- Emacs Muse allows to separate lists with two or more blank lines.
924+ -- Text::Amuse (Amusewiki engine) always creates a single list as of version 0.82.
925+ -- pandoc follows Emacs Muse behavior
926+ , testGroup "Blank lines"
927+ [ "Blank lines between list items are not required" =:
928+ T.unlines
929+ [ " - Foo"
930+ , " - Bar"
931+ ] =?>
932+ bulletList [ para "Foo"
933+ , para "Bar"
934+ ]
935+ , "One blank line between list items is allowed" =:
936+ T.unlines
937+ [ " - Foo"
938+ , ""
939+ , " - Bar"
940+ ] =?>
941+ bulletList [ para "Foo"
942+ , para "Bar"
943+ ]
944+ , "Two blank lines separate lists" =:
945+ T.unlines
946+ [ " - Foo"
947+ , ""
948+ , ""
949+ , " - Bar"
950+ ] =?>
951+ bulletList [ para "Foo" ] <> bulletList [ para "Bar" ]
952+ , "No blank line after multiline first item" =:
953+ T.unlines
954+ [ " - Foo"
955+ , " bar"
956+ , " - Baz"
957+ ] =?>
958+ bulletList [ para "Foo\nbar"
959+ , para "Baz"
960+ ]
961+ , "One blank line after multiline first item" =:
962+ T.unlines
963+ [ " - Foo"
964+ , " bar"
965+ , ""
966+ , " - Baz"
967+ ] =?>
968+ bulletList [ para "Foo\nbar"
969+ , para "Baz"
970+ ]
971+ , "Two blank lines after multiline first item" =:
972+ T.unlines
973+ [ " - Foo"
974+ , " bar"
975+ , ""
976+ , ""
977+ , " - Baz"
978+ ] =?>
979+ bulletList [ para "Foo\nbar" ] <> bulletList [ para "Baz" ]
980+ , "No blank line after list continuation" =:
981+ T.unlines
982+ [ " - Foo"
983+ , ""
984+ , " bar"
985+ , " - Baz"
986+ ] =?>
987+ bulletList [ para "Foo" <> para "bar"
988+ , para "Baz"
989+ ]
990+ , "One blank line after list continuation" =:
991+ T.unlines
992+ [ " - Foo"
993+ , ""
994+ , " bar"
995+ , ""
996+ , " - Baz"
997+ ] =?>
998+ bulletList [ para "Foo" <> para "bar"
999+ , para "Baz"
1000+ ]
1001+ , "Two blank lines after list continuation" =:
1002+ T.unlines
1003+ [ " - Foo"
1004+ , ""
1005+ , " bar"
1006+ , ""
1007+ , ""
1008+ , " - Baz"
1009+ ] =?>
1010+ bulletList [ para "Foo" <> para "bar" ] <> bulletList [ para "Baz" ]
1011+ , "No blank line after blockquote" =:
1012+ T.unlines
1013+ [ " - <quote>"
1014+ , " foo"
1015+ , " </quote>"
1016+ , " - bar"
1017+ ] =?>
1018+ bulletList [ blockQuote $ para "foo", para "bar" ]
1019+ , "One blank line after blockquote" =:
1020+ T.unlines
1021+ [ " - <quote>"
1022+ , " foo"
1023+ , " </quote>"
1024+ , ""
1025+ , " - bar"
1026+ ] =?>
1027+ bulletList [ blockQuote $ para "foo", para "bar" ]
1028+ , "Two blank lines after blockquote" =:
1029+ T.unlines
1030+ [ " - <quote>"
1031+ , " foo"
1032+ , " </quote>"
1033+ , ""
1034+ , ""
1035+ , " - bar"
1036+ ] =?>
1037+ bulletList [ blockQuote $ para "foo" ] <> bulletList [ para "bar" ]
1038+ , "No blank line after verse" =:
1039+ T.unlines
1040+ [ " - > foo"
1041+ , " - bar"
1042+ ] =?>
1043+ bulletList [ lineBlock [ "foo" ], para "bar" ]
1044+ , "One blank line after verse" =:
1045+ T.unlines
1046+ [ " - > foo"
1047+ , ""
1048+ , " - bar"
1049+ ] =?>
1050+ bulletList [ lineBlock [ "foo" ], para "bar" ]
1051+ , "Two blank lines after verse" =:
1052+ T.unlines
1053+ [ " - > foo"
1054+ , ""
1055+ , ""
1056+ , " - bar"
1057+ ] =?>
1058+ bulletList [ lineBlock [ "foo" ] ] <> bulletList [ para "bar" ]
1059+ ]
1060+ -- Test that definition list requires a leading space.
1061+ -- Emacs Muse does not require a space, we follow Amusewiki here.
1062+ , "Not a definition list" =:
1063+ T.unlines
1064+ [ "First :: second"
1065+ , "Foo :: bar"
1066+ ] =?>
1067+ para "First :: second\nFoo :: bar"
1068+ , test emacsMuse "Emacs Muse definition list"
1069+ (T.unlines
1070+ [ "First :: second"
1071+ , "Foo :: bar"
1072+ ] =?>
1073+ definitionList [ ("First", [ para "second" ])
1074+ , ("Foo", [ para "bar" ])
1075+ ])
1076+ , "Definition list" =:
1077+ T.unlines
1078+ [ " First :: second"
1079+ , " Foo :: bar"
1080+ ] =?>
1081+ definitionList [ ("First", [ para "second" ])
1082+ , ("Foo", [ para "bar" ])
1083+ ]
1084+ , "Definition list term cannot include newline" =:
1085+ T.unlines
1086+ [ " Foo" -- "Foo" is not a part of the definition list term
1087+ , " Bar :: baz"
1088+ ] =?>
1089+ para "Foo" <>
1090+ definitionList [ ("Bar", [ para "baz" ]) ]
1091+ , "One-line definition list" =: " foo :: bar" =?>
1092+ definitionList [ ("foo", [ para "bar" ]) ]
1093+ , "Definition list term may include single colon" =:
1094+ " foo:bar :: baz" =?>
1095+ definitionList [ ("foo:bar", [ para "baz" ]) ]
1096+ , "Definition list term with emphasis" =: " *Foo* :: bar\n" =?>
1097+ definitionList [ (emph "Foo", [ para "bar" ]) ]
1098+ , "Definition list term with :: inside code" =: " foo <code> :: </code> :: bar <code> :: </code> baz\n" =?>
1099+ definitionList [ ("foo " <> code " :: ", [ para $ "bar " <> code " :: " <> " baz" ]) ]
1100+ , "Multi-line definition lists" =:
1101+ T.unlines
1102+ [ " First term :: Definition of first term"
1103+ , "and its continuation."
1104+ , " Second term :: Definition of second term."
1105+ ] =?>
1106+ definitionList [ ("First term", [ para "Definition of first term\nand its continuation." ])
1107+ , ("Second term", [ para "Definition of second term." ])
1108+ ]
1109+ , test emacsMuse "Multi-line definition lists from Emacs Muse manual"
1110+ (T.unlines
1111+ [ "Term1 ::"
1112+ , " This is a first definition"
1113+ , " And it has two lines;"
1114+ , "no, make that three."
1115+ , ""
1116+ , "Term2 :: This is a second definition"
1117+ ] =?>
1118+ definitionList [ ("Term1", [ para "This is a first definition\nAnd it has two lines;\nno, make that three."])
1119+ , ("Term2", [ para "This is a second definition"])
1120+ ])
1121+ -- Text::Amuse requires indentation with one space
1122+ , "Multi-line definition lists from Emacs Muse manual with initial space" =:
1123+ (T.unlines
1124+ [ " Term1 ::"
1125+ , " This is a first definition"
1126+ , " And it has two lines;"
1127+ , "no, make that three."
1128+ , ""
1129+ , " Term2 :: This is a second definition"
1130+ ] =?>
1131+ definitionList [ ("Term1", [ para "This is a first definition\nAnd it has two lines;\nno, make that three."])
1132+ , ("Term2", [ para "This is a second definition"])
1133+ ])
1134+ , "One-line nested definition list" =:
1135+ " Foo :: bar :: baz" =?>
1136+ definitionList [ ("Foo", [ definitionList [ ("bar", [ para "baz" ])]])]
1137+ , "Nested definition list" =:
1138+ T.unlines
1139+ [ " First :: Second :: Third"
1140+ , " Fourth :: Fifth :: Sixth"
1141+ , " Seventh :: Eighth"
1142+ ] =?>
1143+ definitionList [ ("First", [ definitionList [ ("Second", [ para "Third" ]),
1144+ ("Fourth", [ definitionList [ ("Fifth", [ para "Sixth"] ) ] ] ) ] ] )
1145+ , ("Seventh", [ para "Eighth" ])
1146+ ]
1147+ , testGroup "Definition lists with multiple descriptions"
1148+ [ "Correctly indented second description" =:
1149+ T.unlines
1150+ [ " First term :: first description"
1151+ , " :: second description"
1152+ ] =?>
1153+ definitionList [ ("First term", [ para "first description"
1154+ , para "second description"
1155+ ])
1156+ ]
1157+ , "Incorrectly indented second description" =:
1158+ T.unlines
1159+ [ " First term :: first description"
1160+ , " :: second description"
1161+ ] =?>
1162+ definitionList [ ("First term", [ para "first description" ])
1163+ , ("", [ para "second description" ])
1164+ ]
1165+ ]
1166+ , "Two blank lines separate definition lists" =:
1167+ T.unlines
1168+ [ " First :: list"
1169+ , ""
1170+ , ""
1171+ , " Second :: list"
1172+ ] =?>
1173+ definitionList [ ("First", [ para "list" ]) ] <>
1174+ definitionList [ ("Second", [ para "list" ]) ]
1175+ -- Headers in first column of list continuation are not allowed
1176+ , "No headers in list continuation" =:
1177+ T.unlines
1178+ [ " - Foo"
1179+ , ""
1180+ , " * Bar"
1181+ ] =?>
1182+ bulletList [ mconcat [ para "Foo"
1183+ , para "* Bar"
1184+ ]
1185+ ]
1186+ , "Bullet list inside a tag" =:
1187+ T.unlines
1188+ [ "<quote>"
1189+ , " - First"
1190+ , ""
1191+ , " - Second"
1192+ , ""
1193+ , " - Third"
1194+ , "</quote>"
1195+ ] =?>
1196+ blockQuote (bulletList [ para "First"
1197+ , para "Second"
1198+ , para "Third"
1199+ ])
1200+ , "Ordered list inside a tag" =:
1201+ T.unlines
1202+ [ "<quote>"
1203+ , " 1. First"
1204+ , ""
1205+ , " 2. Second"
1206+ , ""
1207+ , " 3. Third"
1208+ , "</quote>"
1209+ ] =?>
1210+ blockQuote (orderedListWith (1, Decimal, Period) [ para "First"
1211+ , para "Second"
1212+ , para "Third"
1213+ ])
1214+ -- Regression test for a bug caught by round-trip test
1215+ , "Do not consume whitespace while looking for end tag" =:
1216+ T.unlines
1217+ [ "<quote>"
1218+ , " - <quote>"
1219+ , " foo"
1220+ , " </quote>"
1221+ , " bar" -- Do not consume whitespace while looking for arbitraritly indented </quote>
1222+ , "</quote>"
1223+ ] =?>
1224+ blockQuote (bulletList [ blockQuote $ para "foo" ] <> para "bar")
1225+
1226+ , "Unclosed quote tag" =:
1227+ T.unlines
1228+ [ "<quote>"
1229+ , "<verse>"
1230+ , "</quote>"
1231+ , "</verse>"
1232+ ] =?>
1233+ para "<quote>" <> lineBlock [ "</quote>" ]
1234+
1235+ , "Unclosed quote tag inside list" =:
1236+ T.unlines
1237+ [ " - <quote>"
1238+ , " <verse>"
1239+ , " </quote>"
1240+ , " </verse>"
1241+ ] =?>
1242+ bulletList [ para "<quote>" <> lineBlock [ "</quote>" ] ]
1243+
1244+ -- Allowing indented closing tags is dangerous,
1245+ -- as they may terminate lists
1246+ , "No indented closing tags" =:
1247+ T.unlines
1248+ [ "<quote>"
1249+ , ""
1250+ , " - Foo"
1251+ , ""
1252+ , " </quote>"
1253+ , ""
1254+ , " bar"
1255+ , ""
1256+ , " <verse>"
1257+ , " </quote>"
1258+ , " </verse>"
1259+ ] =?>
1260+ para "<quote>" <> bulletList [ para "Foo" <> para "</quote>" <> para "bar" <> lineBlock [ "</quote>" ] ]
1261+ ]
1262+ ]