Monospace can be used in section headers
18 | This is a paragraph with some words in monospace and in bold monospace.
├── Setup.hs ├── cabal.project ├── .gitmodules ├── examples ├── complex-lists-pandoc.odt ├── asciidoctor-article-template-pandoc.odt ├── monospace.adoc ├── complex-lists.adoc ├── complex-lists-pandoc.md ├── asciidoc-hs.css ├── asciidoctor-article-template-pandoc.md ├── monospace-asciidoctor.html ├── complex-lists-asciidoctor.html ├── monospace-pandoc.html ├── complex-lists-pandoc.html ├── asciidoctor-article-template-asciidoctor.html ├── asciidoctor-article-template.adoc └── asciidoctor-article-template-pandoc.html ├── stack.yaml ├── .gitignore ├── test └── Tests │ ├── Main.hs │ ├── Metadata.hs │ ├── Inlines.hs │ └── Blocks.hs ├── exe └── Main.hs ├── CHANGELOG.adoc ├── stack.yaml.lock ├── src └── Text │ └── AsciiDoc │ ├── UnparsedInline.hs │ ├── Debug │ └── ParseTest.hs │ ├── SourceRange.hs │ ├── Metadata.hs │ ├── LineParsers.hs │ ├── SpecialChars.hs │ ├── ElementAttributes.hs │ ├── Pandoc.hs │ ├── Inlines.hs │ └── Blocks.hs ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── asciidoc-hs.cabal └── README.adoc /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | 3 | main = defaultMain 4 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | ./ 3 | 4 | optional-packages: 5 | ./parsec-free 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "parsec-free"] 2 | path = parsec-free 3 | url = https://github.com/gmarpons/parsec-free.git 4 | -------------------------------------------------------------------------------- /examples/complex-lists-pandoc.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmarpons/asciidoc-hs/HEAD/examples/complex-lists-pandoc.odt -------------------------------------------------------------------------------- /examples/asciidoctor-article-template-pandoc.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmarpons/asciidoc-hs/HEAD/examples/asciidoctor-article-template-pandoc.odt -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-17.14 2 | 3 | packages: 4 | - . 5 | 6 | extra-deps: 7 | - repr-tree-syb-0.1.1@sha256:b3af0d338ab5273ebfc993ab1a3b10a36febffdfad474fb6ae7c2ab1fdb49702,1100 8 | -------------------------------------------------------------------------------- /examples/monospace.adoc: -------------------------------------------------------------------------------- 1 | = Example with monospace 2 | 3 | == `Monospace` can be used in section headers 4 | 5 | This is a paragraph with `some words in monospace and in *bold monospace*`. 6 | -------------------------------------------------------------------------------- /examples/complex-lists.adoc: -------------------------------------------------------------------------------- 1 | = Example with complex lists 2 | 3 | .Unordered list title 4 | * list item 1 5 | - nested list item 6 | ** nested nested list item 1 7 | + 8 | another paragraph in the same nested nested list item 1 9 | ** nested nested list item 2 10 | + 11 | .an example inside item 2 12 | ==== 13 | an example 14 | ==== 15 | 16 | * list item 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-* 3 | cabal-dev 4 | *.o 5 | *.hi 6 | *.hie 7 | *.chi 8 | *.chs.h 9 | *.dyn_o 10 | *.dyn_hi 11 | .hpc 12 | .hsenv 13 | .cabal-sandbox/ 14 | cabal.sandbox.config 15 | *.prof 16 | *.aux 17 | *.hp 18 | *.eventlog 19 | .stack-work/ 20 | cabal.project.local 21 | cabal.project.local~ 22 | .HTF/ 23 | .ghc.environment.* 24 | deps.png 25 | modules.png 26 | cabal.project.local~* 27 | -------------------------------------------------------------------------------- /examples/complex-lists-pandoc.md: -------------------------------------------------------------------------------- 1 | # Example with complex lists 2 | 3 | Unordered list title 4 | 5 | - list item 1 6 | 7 | - nested list item 8 | 9 | - nested nested list item 1 10 | 11 | another paragraph in the same nested nested list item 1 12 | 13 | - nested nested list item 2 14 | 15 | Example 1. an example inside item 2 16 | 17 | an example 18 | 19 | - list item 2 20 | -------------------------------------------------------------------------------- /test/Tests/Main.hs: -------------------------------------------------------------------------------- 1 | module Main 2 | ( main, 3 | ) 4 | where 5 | 6 | import Test.Tasty 7 | import Tests.Blocks 8 | import Tests.Inlines 9 | import Tests.Metadata 10 | 11 | main :: IO () 12 | main = defaultMain tests 13 | 14 | tests :: TestTree 15 | tests = testGroup "tests" [functionalTests] 16 | 17 | functionalTests :: TestTree 18 | functionalTests = 19 | testGroup 20 | "functional tests" 21 | [ blockUnitTests, 22 | inlineUnitTests, 23 | metadataUnitTests 24 | ] 25 | -------------------------------------------------------------------------------- /examples/asciidoc-hs.css: -------------------------------------------------------------------------------- 1 | .monospace { 2 | font-family: "Droid Sans Mono", "DejaVu Sans Mono", monospace; 3 | font-weight: 400; 4 | color: rgba(0, 0, 0, 0.9); 5 | font-size: 1em; 6 | } 7 | 8 | :not(pre):not([class^=L]) > .monospace { 9 | font-size: 0.9375em; 10 | font-style: normal !important; 11 | letter-spacing: 0; 12 | padding: 0.1em 0.5ex; 13 | word-spacing: -0.15em; 14 | background: #f7f7f8; 15 | -webkit-border-radius: 4px; 16 | border-radius: 4px; 17 | line-height: 1.45; 18 | text-rendering: optimizeSpeed; 19 | } 20 | -------------------------------------------------------------------------------- /exe/Main.hs: -------------------------------------------------------------------------------- 1 | module Main 2 | ( main, 3 | ) 4 | where 5 | 6 | import qualified Data.Aeson.Text as Aeson 7 | import qualified Data.Text as T (lines) 8 | import qualified Data.Text.IO as T 9 | import qualified Data.Text.Lazy.IO as LT 10 | import Text.AsciiDoc.Blocks 11 | import Text.AsciiDoc.Pandoc 12 | import qualified Text.Parsec as Parsec (runParser) 13 | 14 | main :: IO () 15 | main = do 16 | result <- 17 | Parsec.runParser documentP blockParserInitialState "" 18 | . T.lines 19 | <$> T.getContents 20 | case result of 21 | Left err -> error $ "Parsing error: " <> show err 22 | Right doc -> 23 | LT.putStrLn $ Aeson.encodeToLazyText $ convertDocument $ parseInlines doc 24 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | = Revision history for asciidoc-hs 2 | 3 | == 0.0.0.0 -- 2021-06-10 4 | 5 | * First version. 6 | * Basic paragraphs. 7 | * Basic, grammar based, inline parsing. 8 | * Conversion to JSON Pandoc. 9 | * (Nested) bold, italic formatting. 10 | * Monospace formatting for HTML output (through support in CSS). 11 | * Highlight formatting and custom inline style. 12 | * Constrained and unconstrained formatting. 13 | * Element attributes: positional, named, shorthand syntax. 14 | * ID attribute, role attribute, optional attribute. 15 | * Block titles. 16 | * Line and block comments. 17 | * Delimited blocks: sidebars, example blocks. 18 | * Section headers and levels. 19 | * Discrete sections. 20 | * Unordered lists (with simple list continuations). 21 | * Document title. 22 | -------------------------------------------------------------------------------- /examples/asciidoctor-article-template-pandoc.md: -------------------------------------------------------------------------------- 1 | # AsciiDoc Article Title 2 | 3 | ## First level heading 4 | 5 | This is a paragraph with a **bold** word and an *italicized* word. 6 | 7 | ### Second level heading 8 | 9 | Unordered list title 10 | 11 | - list item 1 12 | 13 | - nested list item 14 | 15 | - nested nested list item 1 16 | 17 | - nested nested list item 2 18 | 19 | - list item 2 20 | 21 | This is a paragraph. 22 | 23 | Example 1. Example block title 24 | 25 | Content in an example block is subject to normal substitutions. 26 | 27 | Sidebar title 28 | 29 | Sidebars contain aside text and are subject to normal substitutions. 30 | 31 | #### Third level heading 32 | 33 | ##### Fourth level heading 34 | 35 | ###### Fifth level heading 36 | 37 | ## First level heading 38 | 39 | ## First level heading 40 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: repr-tree-syb-0.1.1@sha256:b3af0d338ab5273ebfc993ab1a3b10a36febffdfad474fb6ae7c2ab1fdb49702,1100 9 | pantry-tree: 10 | size: 213 11 | sha256: 9a20743b457ff2e3e1ce98881480f8353b7837c66352cf55c1edd2a8dac97fd4 12 | original: 13 | hackage: repr-tree-syb-0.1.1@sha256:b3af0d338ab5273ebfc993ab1a3b10a36febffdfad474fb6ae7c2ab1fdb49702,1100 14 | snapshots: 15 | - completed: 16 | size: 567677 17 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/14.yaml 18 | sha256: 3740f22286bf5e6e3d82f88125e1c708b6e27847211f956b530aa5d83cf39383 19 | original: lts-17.14 20 | -------------------------------------------------------------------------------- /examples/monospace-asciidoctor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |Monospace can be used in section headersThis is a paragraph with some words in monospace and in bold monospace.
list item 1
21 |nested list item
25 |nested nested list item 1
29 |another paragraph in the same nested nested list item 1
31 |nested nested list item 2
35 |an example
40 |list item 2
52 |This is a paragraph with some words in monospace and in bold monospace.
34 |list item 1
38 |nested list item
43 |nested nested list item 1
48 |another paragraph in the same nested nested list item 1
51 |nested nested list item 2
54 |an example
62 |list item 2
71 |This is a paragraph with a bold word and an italicized word.
21 |list item 1
29 |nested list item
33 |nested nested list item 1
37 |nested nested list item 2
40 |list item 2
49 |This is a paragraph.
54 |Content in an example block is subject to normal substitutions.
60 |This is a paragraph with a bold word and an italicized word.
33 |list item 1
42 |nested list item
47 |nested nested list item 1
52 |nested nested list item 2
55 |list item 2
62 |This is a paragraph.
67 |Content in an example block is subject to normal substitutions.
75 | element does. Among the three following solutions, we choose 1)
207 | -- because it's simpler than 2) and produces cleaner (probably more robust for
208 | -- backends that are not HTML) Pandoc output, and with 3) a lot of markup is
209 | -- potentially lost. Also, it's congruent with the treatment of above:
210 | --
211 | -- 1) Convert Monospace into a span, do not call Pandoc.code, and add a
212 | -- "monospace" class. I.e., do not generate element in HTML and
213 | -- delegate monospace markup to CSS or another formatting mechanism.
214 | --
215 | -- 2) Break inlines into pieces of uniform markup, and apply Pandoc.code as
216 | -- the inner-most construct of each piece. Apply other markup to the pieces
217 | -- that need it.
218 | --
219 | -- 3) Convert inlines into a Text with no markup applied.
220 | --
221 | -- TODO. Find a solution that can work for other writers than HTML.
222 | StyledText Monospace as _ inlines _ ->
223 | Pandoc.spanWith (toAttr $ mempty {metadataRoles = ["monospace"]} <> toMetadata as) $
224 | foldMap convertInline inlines
225 | Symbol t -> Pandoc.str t
226 |
227 | toAttr :: Metadata UnparsedInline -> Attr
228 | toAttr m = (identifier, classes, keyvals)
229 | where
230 | identifier = case metadataIds m of
231 | [] -> ""
232 | (x : _) -> x -- TODO. Support multiple id's per entity.
233 | classes = metadataRoles m
234 | -- We could do
235 | -- Map.toList $ metadataNamedAttributes m
236 | -- but keyvals is used by Pandoc for data-* attributes.
237 | keyvals = []
238 |
239 | -- | In case the metadata contains a block title, this function prepends a div
240 | -- with the title to the provided 'Pandoc.Blocks' value.
241 | prependTitleDiv :: Metadata Inline -> Pandoc.Blocks -> Pandoc.Blocks
242 | prependTitleDiv m = case metadataTitle m of
243 | Nothing -> id
244 | Just (Last i) ->
245 | mappend $
246 | Pandoc.divWith (toAttr $ mempty {metadataRoles = ["title"]}) $
247 | Pandoc.plain $ convertInline i
248 |
--------------------------------------------------------------------------------
/src/Text/AsciiDoc/Inlines.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DerivingStrategies #-}
2 | {-# LANGUAGE GADTs #-}
3 | {-# LANGUAGE LambdaCase #-}
4 | {-# LANGUAGE OverloadedStrings #-}
5 |
6 | -- |
7 | -- Module : Text.AsciiDoc.Inlines
8 | -- Copyright : © 2020–present Guillem Marpons
9 | -- License : BSD-3-Clause
10 | --
11 | -- Maintainer : Guillem Marpons
12 | -- Stability : experimental
13 | -- Portability : portable
14 | --
15 | -- This module contains Parsec-style parsers for AsciiDoc inline elements.
16 | --
17 | -- It tries to be compatible with Asciidoctor, but uses a grammar-based parsing
18 | -- approach instead of regexes.
19 | --
20 | -- There are three kinds of terminals in the grammar:
21 | --
22 | -- * Alpha-numeric sequences ('AlphaNum').
23 | -- * Gaps: space-like character sequences and 'Newline's (including the
24 | -- surrounding space).
25 | -- * Other characters: punctuation symbols, mathematical symbols, etc.
26 | -- It includes formatting and punctuation marks.
27 | --
28 | -- These groups of characters govern how constrained enclosures are parsed.
29 | module Text.AsciiDoc.Inlines
30 | ( -- * AST types
31 | Inline (..),
32 | Style (..),
33 | InlineAttributeList (..),
34 | defaultAttributeList,
35 |
36 | -- * Parsers
37 | inlinesP,
38 |
39 | -- * Parser type
40 | Parser,
41 | State (..),
42 | inlineParserInitialState,
43 | )
44 | where
45 |
46 | import Control.Monad (when)
47 | import Control.Monad.Combinators (someTill)
48 | import Control.Monad.Combinators hiding
49 | ( endBy1,
50 | sepBy1,
51 | sepEndBy1,
52 | some,
53 | someTill,
54 | )
55 | import Control.Monad.Combinators.NonEmpty (some)
56 | import Data.Char (isAlphaNum, isSpace)
57 | import Data.Functor (void)
58 | import Data.Generics (Data, Typeable)
59 | import Data.List.NonEmpty (NonEmpty (..), (<|))
60 | import qualified Data.List.NonEmpty as NE
61 | import Data.Text (Text)
62 | import qualified Data.Text as T
63 | import Text.AsciiDoc.ElementAttributes
64 | import Text.AsciiDoc.Metadata
65 | import Text.AsciiDoc.SpecialChars
66 | import Text.AsciiDoc.UnparsedInline
67 | import Text.Parsec ((>))
68 | import qualified Text.Parsec as Parsec
69 | ( ParsecT,
70 | char,
71 | eof,
72 | getState,
73 | label,
74 | lookAhead,
75 | notFollowedBy,
76 | parse,
77 | putState,
78 | try,
79 | )
80 | import qualified Text.Parsec.Char as Parsec
81 | ( anyChar,
82 | satisfy,
83 | string,
84 | )
85 |
86 | type Parser m = Parsec.ParsecT Text State m
87 |
88 | -- | Custom parser state for the parser for 'Inline's.
89 | newtype State = State
90 | { -- | A stack (LIFO) of formatting and punctuation 'Mark's.
91 | --
92 | -- Every mark in the stack is the opening mark for the corresponding
93 | -- enclosure.
94 | -- Top of the stack contains the opening mark for most recently open
95 | -- enclosure.
96 | openEnclosures :: [Mark]
97 | }
98 | deriving newtype (Eq, Show)
99 |
100 | inlineParserInitialState :: State
101 | inlineParserInitialState =
102 | State
103 | { openEnclosures = []
104 | }
105 |
106 | -- | Formatting styles for inline text.
107 | data Style
108 | = Bold
109 | | Custom
110 | | Italic
111 | | Monospace
112 | deriving stock (Eq, Show, Typeable, Data)
113 |
114 | -- | Subscript
115 | -- | Superscript
116 |
117 | -- | Every formatting 'Mark' maps to a 'Style', and this function computes this
118 | -- map.
119 | toStyle :: Mark -> Style
120 | toStyle = \case
121 | SingleMark NumberF -> Custom
122 | SingleMark AsteriskF -> Bold
123 | SingleMark UnderscoreF -> Italic
124 | SingleMark GraveF -> Monospace
125 | DoubleMark NumberF -> Custom
126 | DoubleMark AsteriskF -> Bold
127 | DoubleMark UnderscoreF -> Italic
128 | DoubleMark GraveF -> Monospace
129 |
130 | -- | A data type for all the different inline types.
131 | --
132 | -- Some inline types can contain other inlines, and there is a constructor
133 | -- 'InlineSeq' serving as a general inline container.
134 | data Inline
135 | = AlphaNum Text
136 | | EndOfInline Text
137 | | InlineSeq (NonEmpty Inline)
138 | | Newline Text
139 | | Space Text
140 | | StyledText Style InlineAttributeList Text (NonEmpty Inline) Text
141 | | Symbol Text
142 | deriving stock (Eq, Show, Typeable, Data)
143 |
144 | -- | InlineMacro Text
145 | -- | EscapedSymbol Text
146 | -- | DoubleEscapedSymbol Text
147 |
148 | instance Semigroup Inline where
149 | InlineSeq x <> InlineSeq y = InlineSeq (x <> y)
150 | InlineSeq x <> b = InlineSeq $ x <> (b :| [])
151 | a <> InlineSeq y = InlineSeq $ a <| y
152 | a <> b = InlineSeq $ a :| [b]
153 |
154 | -- EBNF grammar non-terminal symbols ------------------------------------------
155 |
156 | -- The parser can be read as an EBNF grammar, with starting symbol 'inlinesP'.
157 | -- The resulting grammar would be ambiguous, so there are a series of functions
158 | -- used to discard some parsing paths ('muP', 'piP', 'sigmaP', 'phiP', 'psiP',
159 | -- 'omegaP'), that work together with order in alternatives to give a completely
160 | -- deterministic parser.
161 |
162 | -- | This function is the only exported parser function for AsciiDoc inlines.
163 | --
164 | -- If more than one individual inlines can be parsed, it returns all of them
165 | -- encapsulated into an 'InlineSeq'.
166 | inlinesP :: Monad m => Parser m Inline
167 | inlinesP =
168 | (\(x :| xs) ys -> InlineSeq (x :| xs ++ ys))
169 | <$> (firstP > "F")
170 | <*> ( concat
171 | <$> many
172 | ( NE.toList <$> Parsec.label gapWithOptionalContinuationP "N1"
173 | <|> Parsec.label (sigmaP *> nonGapSequenceP) "N2"
174 | )
175 | )
176 | <* Parsec.eof
177 |
178 | unconstrainedP :: Monad m => Parser m Inline
179 | unconstrainedP = Parsec.try $ do
180 | Parsec.label (pure ()) "U"
181 | ps <- option defaultAttributeList inlineAttributeListP
182 | phiP
183 | openMark <- openP ["##", "**", "__", "``"]
184 | is <- inlinesInUnconstrainedP
185 | closeMark <- closeP openMark
186 | pure $ StyledText (toStyle openMark) ps (fromMarkT openMark) is (fromMarkT closeMark)
187 | where
188 | fromMarkT = T.pack . fromMark
189 | inlinesInUnconstrainedP =
190 | (\(x :| xs) ys -> x :| xs ++ ys)
191 | <$> ((gapWithOptionalContinuationP <|> firstP) > "Y_p | F")
192 | <*> ( concat
193 | <$> many
194 | ( NE.toList <$> Parsec.label gapWithOptionalContinuationP "N1"
195 | <|> Parsec.label (sigmaP *> nonGapSequenceP) "N2"
196 | )
197 | )
198 | {-# ANN unconstrainedP ("HLint: ignore" :: String) #-}
199 |
200 | constrainedP :: Monad m => Parser m Inline
201 | constrainedP = Parsec.try $ do
202 | Parsec.label (pure ()) "C"
203 | ps <- option defaultAttributeList inlineAttributeListP
204 | varphiP
205 | openMark <- openP ["#", "*", "_", "`"]
206 | is <- inlinesInConstrainedP
207 | omegaP
208 | closeMark <- closeP openMark
209 | pure $ StyledText (toStyle openMark) ps (fromMarkT openMark) is (fromMarkT closeMark)
210 | where
211 | fromMarkT = T.pack . fromMark
212 | inlinesInConstrainedP =
213 | (\(x :| xs) ys -> x :| xs ++ ys)
214 | <$> (firstP > "F")
215 | <*> ( concat
216 | <$> many
217 | ( Parsec.label gapWithContinuationP "N1"
218 | <|> Parsec.label (muP *> nonGapSequenceP) "N2"
219 | )
220 | )
221 | {-# ANN constrainedP ("HLint: ignore" :: String) #-}
222 |
223 | firstP :: Monad m => Parser m (NonEmpty Inline)
224 | firstP =
225 | -- Notice the similarity with alphaNumOrOtherP.
226 | (:| [])
227 | <$> alphaNumP
228 | <|> piP *> otherWithContinuationP
229 |
230 | nonGapSequenceP :: Monad m => Parser m [Inline]
231 | nonGapSequenceP =
232 | (:)
233 | <$> (unconstrainedP <|> otherP)
234 | <*> option [] alphaNumOrOtherP
235 |
236 | openP :: Monad m => [Mark] -> Parser m Mark
237 | openP ms = do
238 | Parsec.label (pure ()) "M_<"
239 | mark <- choice $ Parsec.try . markP <$> ms
240 | -- What follows is not part of the EBNF description of the language, but it's
241 | -- easier to put it here than create a specific function for it.
242 | st <- Parsec.getState
243 | Parsec.putState $ st {openEnclosures = mark : openEnclosures st}
244 | Parsec.label (pure ()) $ "State: " ++ show (mark : openEnclosures st)
245 | pure mark
246 |
247 | closeP :: Monad m => Mark -> Parser m Mark
248 | closeP openMark = do
249 | -- Passing a mark to this function is redundant, but the openP/closeP
250 | -- connection makes the interface more clear for callers. It can also be used
251 | -- to (run-time) check for some programming errors.
252 | Parsec.label (pure ()) $ "M_>: " ++ show openMark
253 | let closeMark = closingMarkOf openMark
254 | _ <- markP closeMark
255 | -- What follows is not part of the EBNF description of the language, but it's
256 | -- easier to put it here than create a specific function for it.
257 | st <- Parsec.getState
258 | case openEnclosures st of
259 | (e : es) -> do
260 | when (closingMarkOf e /= closeMark) $
261 | error "closeP: trying to close mark different from the innermost enclosure"
262 | Parsec.putState $ st {openEnclosures = es}
263 | [] -> error "closeP: trying to close non-existent enclosure"
264 | pure closeMark
265 |
266 | gapWithContinuationP :: Monad m => Parser m [Inline]
267 | gapWithContinuationP =
268 | flip Parsec.label "Y" $
269 | -- Parsec.try is necessary because we can accept gapP and fail afterwards.
270 | Parsec.try $
271 | (++)
272 | <$> (NE.toList <$> gapP)
273 | <*> ((: []) <$> alphaNumP <|> NE.toList <$ sigmaP <*> otherWithContinuationP)
274 |
275 | gapWithOptionalContinuationP :: Monad m => Parser m (NonEmpty Inline)
276 | gapWithOptionalContinuationP =
277 | flip Parsec.label "Y_p" $
278 | (\(h :| t) c -> h :| t ++ c)
279 | <$> gapP
280 | <*> option [] ((: []) <$> alphaNumP <|> NE.toList <$ sigmaP <*> otherWithContinuationP)
281 |
282 | otherWithContinuationP :: Monad m => Parser m (NonEmpty Inline)
283 | otherWithContinuationP =
284 | flip Parsec.label "X" $
285 | ((:|) <$> unconstrainedP <*> option [] alphaNumOrOtherP)
286 | <|> ((:|) <$> constrainedP <*> option [] gapOrOtherWithContinuationP)
287 | <|> ((:|) <$> otherP <*> option [] alphaNumOrOtherP)
288 | where
289 | gapOrOtherWithContinuationP =
290 | flip Parsec.label "Y | X" $
291 | gapWithContinuationP
292 | <|> muP *> (NE.toList <$> otherWithContinuationP)
293 |
294 | alphaNumOrOtherP :: Monad m => Parser m [Inline]
295 | alphaNumOrOtherP =
296 | flip Parsec.label "A | X" $
297 | (: [])
298 | <$> alphaNumP
299 | <|> muP *> (NE.toList <$> otherWithContinuationP)
300 |
301 | gapP :: Monad m => Parser m (NonEmpty Inline)
302 | gapP = flip Parsec.label "G" $ some newlineOrSpaceP
303 | where
304 | newlineOrSpaceP =
305 | newlineP
306 | -- Inline the definition of spaceP here allows to avoid redundant checks
307 | -- against '\n' or '\r'.
308 | <|> Space . T.pack . NE.toList <$> some (Parsec.satisfy isSpace)
309 |
310 | -- Functions for disambiguating the EBNF grammar ------------------------------
311 |
312 | -- | Function called after a character of kind alphanum or other, and before a
313 | -- character of kind other.
314 | --
315 | -- TODO. Check that this function together with 'sigmaP' cover all cases at the
316 | -- beginning of N
317 | --
318 | -- It fails if an open enclosure can be closed (using 'closableMarks'), or a
319 | -- full unconstrained enclosure can be parsed, at current input.
320 | muP :: Monad m => Parser m ()
321 | muP = do
322 | Parsec.label (pure ()) "MU"
323 | st <- Parsec.getState
324 | Parsec.notFollowedBy (choice $ tryToCloseMarkP <$> closableMarks (openEnclosures st))
325 | <|> Parsec.lookAhead (void unconstrainedP)
326 |
327 | -- | Function called after the opening mark of an enclosure (i.e., after a
328 | -- character of kind other), and before a character of kind other.
329 | --
330 | -- It's identical to 'muP' with the exception that the mark recently open (top
331 | -- of the stack) is not taken into account.
332 | --
333 | -- It fails if an open enclosure (except the innermost one) can be closed (using
334 | -- 'closableMarks'), or a full unconstrained enclosure can be parsed, at current
335 | -- input.
336 | piP :: Monad m => Parser m ()
337 | piP = do
338 | Parsec.label (pure ()) "PI"
339 | st <- Parsec.getState
340 | case openEnclosures st of
341 | (_ : es) ->
342 | Parsec.notFollowedBy (choice $ tryToCloseMarkP <$> closableMarks es)
343 | <|> Parsec.lookAhead (void unconstrainedP)
344 | [] -> pure ()
345 |
346 | -- | Function called after a character of kind gap, and before a character of
347 | -- kind other.
348 | --
349 | -- It fails if any unconstrained open enclosure can be closed.
350 | sigmaP :: Monad m => Parser m ()
351 | sigmaP = do
352 | Parsec.label (pure ()) "SIGMA"
353 | st <- Parsec.getState
354 | Parsec.notFollowedBy $
355 | choice $ tryToCloseMarkP <$> filter isUnconstrained (openEnclosures st)
356 |
357 | -- | Function called before the opening mark for an unconstrained enclosure
358 | -- (i.e., before a character of kind other)
359 | --
360 | -- It fails if we're trying to open an already open mark.
361 | --
362 | -- It takes into account the case that we can open both an arleady open mark and
363 | -- an extension of it (e.g., "@**@" is an extension of "@*@"). This function
364 | -- doesn't fail in this case, and the parser will try to open the extended mark.
365 | phiP :: Monad m => Parser m ()
366 | phiP = do
367 | Parsec.label (pure ()) "PHI"
368 | st <- Parsec.getState
369 | Parsec.notFollowedBy (choice $ markP <$> openEnclosures st)
370 | <|> void (Parsec.lookAhead (choice $ markP <$> concatMap extendedMarksOf (openEnclosures st)))
371 |
372 | -- | Function called before the opening mark for a constrained enclosure (i.e.,
373 | -- before a character of kind other).
374 | --
375 | -- It fails if we're trying to open an already open mark.
376 | varphiP :: Monad m => Parser m ()
377 | varphiP = do
378 | Parsec.label (pure ()) "VARPHI"
379 | st <- Parsec.getState
380 | Parsec.notFollowedBy $ choice $ markP <$> openEnclosures st
381 |
382 | -- | Function called after a character of kind alphanum or other, and before the
383 | -- closing mark for a constrained enclosure (i.e., before a character of kind
384 | -- other).
385 | --
386 | -- It fails if we can close an open extended version (i.e., an unconstrained
387 | -- enclosure) of the mark we're trying to close, as closing the unconstrained
388 | -- enclosure takes priority.
389 | omegaP :: Monad m => Parser m ()
390 | omegaP = do
391 | Parsec.label (pure ()) "OMEGA"
392 | st <- Parsec.getState
393 | Parsec.notFollowedBy $
394 | choice $ tryToCloseMarkP <$> filter isUnconstrained (openEnclosures st)
395 |
396 | closableMarks :: [Mark] -> [Mark]
397 | closableMarks ms = x ++ filter isUnconstrained y
398 | where
399 | (x, y) = span isConstrained ms
400 |
401 | tryToCloseMarkP :: Monad m => Mark -> Parser m ()
402 | tryToCloseMarkP m = Parsec.try $ do
403 | Parsec.label (pure ()) $ "tryToCloseMarkP: " ++ show m
404 | _ <- markP $ closingMarkOf m
405 | when (isConstrained m) $ do
406 | Parsec.eof <|> void (Parsec.satisfy (not . isAlphaNum))
407 |
408 | -- EBNF grammar terminal symbols ----------------------------------------------
409 |
410 | markP :: Monad m => Mark -> Parser m Mark
411 | markP m = m <$ Parsec.string (fromMark m)
412 |
413 | otherP :: Monad m => Parser m Inline
414 | otherP =
415 | Symbol . T.singleton
416 | <$> Parsec.satisfy (\c -> not (isSpace c || isAlphaNum c))
417 |
418 | -- | It parses as newlines the combinations:
419 | --
420 | -- * @CR@
421 | -- * @CR LF@
422 | -- * @LF@
423 | --
424 | -- This is the exact set parsed by @libasciidoc@. At the moment we do not
425 | -- consider the combination @LF CR@ (used in some systems, see
426 | -- https://en.wikipedia.org/wiki/Newline#Representation) as a single newline.
427 | newlineP :: Monad m => Parser m Inline
428 | newlineP =
429 | wrap <$> newlineP' <*> optional Parsec.eof
430 | where
431 | wrap t Nothing = Newline t
432 | wrap t (Just ()) = EndOfInline t
433 | newlineP' :: Monad m => Parser m Text
434 | newlineP' =
435 | (<>) <$> singletonP '\r' <*> option "" (singletonP '\n')
436 | <|> singletonP '\n'
437 | singletonP :: Monad m => Char -> Parser m Text
438 | singletonP c = T.singleton <$> Parsec.char c
439 |
440 | alphaNumP :: Monad m => Parser m Inline
441 | alphaNumP =
442 | AlphaNum . T.pack . NE.toList
443 | <$> Parsec.label (some wordCharP) "A"
444 | where
445 | wordCharP = Parsec.satisfy $ \c ->
446 | isAlphaNum c
447 |
448 | -- Parser for element attribute (aka parameter) lists ------------------------
449 |
450 | newtype InlineAttributeList = InlineAttributeList Text
451 | deriving newtype (Eq, Show)
452 | deriving stock (Data, Typeable)
453 |
454 | -- | This instance accepts the same kind of attributes than the instance for
455 | -- 'Text.AsciiDoc.Blocks.BlockPrefixItem's, including the shorthand syntax.
456 | --
457 | -- Attributes @title@ and @opts@/@options@ have currently no meaning for
458 | -- inlines, but they are still parsed and stored in the resulting 'Metadata'
459 | -- value.
460 | --
461 | -- __Divergence from Asciidoctor__: The aforementioned behavior implies that
462 | -- some inputs produce different results than Asciidoctor.
463 | -- Asciidoctor only honours @role@ and @id@ attributes and messes up the rest.
464 | instance ToMetadata InlineAttributeList UnparsedInline where
465 | toMetadata (InlineAttributeList "") = mempty
466 | toMetadata (InlineAttributeList t) =
467 | case Parsec.parse attributeListP "" t of
468 | Right attributes ->
469 | toMetadata $ PositionedAttribute <$> NE.zip (1 :| [2 ..]) attributes
470 | Left _ -> error "toMetadata @InlineAttributeList: parse should not fail"
471 |
472 | defaultAttributeList :: InlineAttributeList
473 | defaultAttributeList = InlineAttributeList ""
474 |
475 | -- | Accepts an square-bracket-enclosed string with no restrictions on the
476 | -- characters in between, provided that there is at least one such character.
477 | inlineAttributeListP :: Monad m => Parser m InlineAttributeList
478 | inlineAttributeListP =
479 | flip Parsec.label "P" $
480 | InlineAttributeList . T.pack
481 | <$ Parsec.char '[' <*> someTill Parsec.anyChar (Parsec.char ']')
482 |
--------------------------------------------------------------------------------
/src/Text/AsciiDoc/Blocks.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DerivingStrategies #-}
2 | {-# LANGUAGE GADTs #-}
3 | {-# LANGUAGE OverloadedStrings #-}
4 |
5 | -- |
6 | -- Module : Text.AsciiDoc.Blocks
7 | -- Copyright : © 2020–present Guillem Marpons
8 | -- License : BSD-3-Clause
9 | --
10 | -- Maintainer : Guillem Marpons
11 | -- Stability : experimental
12 | -- Portability : portable
13 | --
14 | -- This module contains Parsec-style parsers for AsciiDoc block elements.
15 | --
16 | -- It tries to be compatible with Asciidoctor.
17 | module Text.AsciiDoc.Blocks
18 | ( -- * AST types
19 | HeaderLevel,
20 | ListType (..),
21 | ListCheckStatus (..),
22 | NestableBlockType (..),
23 | AdmonitionType (..),
24 | LiteralBlockType (..),
25 | LiteralIndentation (..),
26 | BlockMacroType (..),
27 | MacroArguments (..),
28 | IncludeOptions (..),
29 | AttributeId,
30 | Comment (..),
31 | MetadataItem (..),
32 | BlockPrefixItem (..),
33 | UnparsedBlockPrefix,
34 | Block (..),
35 | DocumentHeader (..),
36 | Document (..),
37 |
38 | -- * Parsers
39 | documentP,
40 | documentHeaderP,
41 | blocksP,
42 | blockP,
43 | blockPrefixP,
44 | attributeEntryP,
45 | blockIdP,
46 | blockAttributeListP,
47 | blockTitleP,
48 | nestableP,
49 | sectionHeaderP,
50 | paragraphP,
51 | danglingBlockPrefixP,
52 | initialBlankLinesP,
53 | blankLineP,
54 |
55 | -- * Parser type
56 | Parser,
57 | State (..),
58 | blockParserInitialState,
59 |
60 | -- * Helper low-level parsers
61 | lineP,
62 | lineP',
63 | lineOneOfP,
64 | lineNoneOfP,
65 | includeP,
66 | openDelimiterP,
67 | closeDelimiterP,
68 | satisfyToken,
69 | )
70 | where
71 |
72 | import Control.Arrow (Arrow ((&&&)))
73 | import Control.Monad.Combinators hiding
74 | ( endBy1,
75 | sepBy1,
76 | sepEndBy1,
77 | some,
78 | someTill,
79 | )
80 | import Control.Monad.Combinators.NonEmpty
81 | import Data.Char (isSpace)
82 | import Data.List.NonEmpty (NonEmpty (..), (<|))
83 | import qualified Data.List.NonEmpty as NE
84 | import qualified Data.Map as Map
85 | import Data.Maybe (catMaybes)
86 | import Data.Semigroup (Last (..))
87 | import Data.Text (Text)
88 | import qualified Data.Text as T
89 | import Text.AsciiDoc.ElementAttributes
90 | import qualified Text.AsciiDoc.LineParsers as LP
91 | import Text.AsciiDoc.Metadata
92 | import Text.AsciiDoc.SpecialChars
93 | import Text.AsciiDoc.UnparsedInline
94 | import Text.Parsec ((>))
95 | import qualified Text.Parsec as Parsec
96 | import Text.Parsec.Char (alphaNum, char, space)
97 |
98 | -- | Greater or equal than 0.
99 | -- A section header prefixed by one single "@=@" has level 0, and one with two
100 | -- "@=@"'s has level 1.
101 | -- This follows Asciidoctor's behavior.
102 | type HeaderLevel = Int
103 |
104 | -- Text: can contain symbols, does not begin nor end with space.
105 | -- Text': can end with spaces.
106 | data ListType
107 | = Description -- PEG: Space* Text Space* "::" (Space+ Text)? Space*
108 | | Ordered -- PEG: Space* "."+ Space+ Text'
109 | | Unordered (Maybe ListCheckStatus) -- PEG: Space* ("*"+ / "-"+ / ... ) Space+ Text'
110 | | -- | Callouts can be conceptualized as belonging to the block they follow
111 | -- from, but Asciidoctor treats them as an independent entity, very similar to
112 | -- any other list.
113 | Callout -- PEG: "<" (Num / ".") ">" Space+ Text'
114 | deriving stock (Eq, Show)
115 |
116 | data ListCheckStatus
117 | = Checked
118 | | Unchecked
119 | deriving stock (Eq, Show)
120 |
121 | data NestableBlockType
122 | = Admonition AdmonitionType
123 | | Example
124 | | Sidebar
125 | | Quote
126 | | -- | Open block (delimited with "--") with non-standard name.
127 | Other Text
128 | deriving stock (Eq, Show)
129 |
130 | data AdmonitionType
131 | = Note
132 | | Tip
133 | | Important
134 | | Caution
135 | | Warning
136 | deriving stock (Eq, Show)
137 |
138 | -- | Literal block types are subject by default to substitution group
139 | -- "Verbatim", if not stated otherwise. The actual substitutions applied can be
140 | -- modified with the @subs@ block attribute, nonetheless.
141 | data LiteralBlockType
142 | = Fenced
143 | | Listing
144 | | Literal LiteralIndentation
145 | | -- | Default substitution group: None (aka Passthrough).
146 | Passthrough
147 | | Source
148 | | -- | Default substitution group: None (aka Passthrough).
149 | Stem
150 | | Verse
151 | deriving stock (Eq, Show)
152 |
153 | -- | The @Int@ is the indentation of the block. If the @Literal@ block is not
154 | -- signaled by indentation (i.e., @....@ or @[literal]@ is used), then
155 | -- indentation is 0 (all preceding space is copied verbatim as content).
156 | newtype LiteralIndentation = LiteralIndentation Int
157 | deriving newtype (Eq, Show)
158 |
159 | data BlockMacroType
160 | = ImageBlockMacro
161 | | TableOfContentsMacro
162 | | CustomBlockMacro
163 | deriving stock (Eq, Show)
164 |
165 | data MacroArguments = MacroArguments
166 | deriving stock (Eq, Show)
167 |
168 | data IncludeOptions
169 | = IncludeOptions
170 | deriving stock (Eq, Show)
171 |
172 | type AttributeId = Text
173 |
174 | data Comment
175 | = LineCommentSequence (NonEmpty Text)
176 | | BlockComment [Text]
177 | deriving stock (Eq, Show)
178 |
179 | -- | A Block can be preceded by an arbitrary (finite) list of @MetadataItem@s.
180 | --
181 | -- This is a syntactic element. Every value of this type comes from a source
182 | -- line.
183 | data MetadataItem a
184 | = -- | A block can have more than one ID (aka anchor), and all of them can be
185 | -- used in cross-references.
186 | BlockId Text
187 | | -- | A block can be preceded by any number of @BlockTitle@s (aka labels).
188 | -- Only the last one is semantically relevant.
189 | BlockTitle a
190 | | -- | A block can be preceded by any number of @BlockAttributeList@s. For
191 | -- positional arguments, only the last list is taken into account.
192 | --
193 | -- Some of the elements of the list can be name-value pairs.
194 | --
195 | -- TODO. Check if some attributes in the list can contain full inlines, as
196 | -- it's the case with standalone (aka attribute entry) attributes.
197 | BlockAttributeList Text
198 | deriving stock (Eq, Show, Functor)
199 |
200 | instance ToMetadata (MetadataItem UnparsedInline) UnparsedInline where
201 | toMetadata (BlockId i) = mempty {metadataIds = [i]}
202 | toMetadata (BlockTitle t) = mempty {metadataTitle = Just $ Last t}
203 | toMetadata (BlockAttributeList "") = mempty
204 | toMetadata (BlockAttributeList t) =
205 | case Parsec.parse attributeListP "" t of
206 | Right attributes ->
207 | toMetadata $ PositionedAttribute <$> NE.zip (1 :| [2 ..]) attributes
208 | Left _ -> error "toMetadata @(MetadataItem UnparsedInline): parse should not fail"
209 |
210 | data BlockPrefixItem a
211 | = MetadataItem (MetadataItem a)
212 | | -- | A value of @Nothing@ means the attribute has been unset.
213 | AttributeEntry AttributeId (Maybe a)
214 | | Comment Comment
215 | deriving stock (Eq, Show, Functor)
216 |
217 | instance ToMetadata (BlockPrefixItem UnparsedInline) UnparsedInline where
218 | toMetadata (MetadataItem x) = toMetadata x
219 | toMetadata (AttributeEntry _ _) = mempty
220 | toMetadata (Comment _) = mempty
221 |
222 | type UnparsedBlockPrefix = [BlockPrefixItem UnparsedInline]
223 |
224 | -- | A Block consists, syntactically, of one or more contiguous and complete
225 | -- lines of text.
226 | -- Some block types can contain other blocks.
227 | data Block a
228 | = -- | Regular paragraph.
229 | Paragraph UnparsedBlockPrefix a
230 | | -- | This data constructor is not used during parsing, it requires an
231 | -- additional "nesting" pass.
232 | --
233 | -- There can be a @Section@ inside an, e.g., open block, but it needs to
234 | -- have style @discrete@.
235 | Section UnparsedBlockPrefix HeaderLevel a [Block a]
236 | | -- | A section header contains the same information as a section, except the
237 | -- contained sequence of blocks.
238 | --
239 | -- After the "nesting" pass, all @SectionHeader@s but @discrete@ ones are
240 | -- converted to proper @Section@s.
241 | SectionHeader UnparsedBlockPrefix HeaderLevel a
242 | | List ListType UnparsedBlockPrefix (NonEmpty (NonEmpty (Block a)))
243 | | {- Table -- TODO. Many things here -}
244 | {- ThematicBreak UnparsedBlockPrefix -}
245 | {- PageBreak UnparsedBlockPrefix -}
246 |
247 | -- | Sequence of blocks of some defined type that allows nested blocks
248 | -- inside (i.e. admonition, sidebar, example, quote, and open block with no
249 | -- other standard type).
250 | Nestable NestableBlockType UnparsedBlockPrefix [Block a]
251 | | {- VerseBlock UnparsedBlockPrefix [a] -}
252 | {- -- | Block type determines substitution group applied: @Verbatim@ or @None@
253 | -- (aka passthrough).
254 | --
255 | -- TODO: Check that designed pipeline guarantees that pre-processor
256 | -- directives are expanded (if not escaped) even in literal blocks, as
257 | -- https://asciidoctor.org/docs/user-manual/#include-processing states.
258 | LiteralBlock LiteralBlockType UnparsedBlockPrefix [Text] -}
259 |
260 | {- -- | Some macros accept block metadata, as e.g. @toc::[]@, that accepts
261 | -- defining its title with @.TITLE@ syntax.
262 | BlockMacro BlockMacroType UnparsedBlockPrefix MacroArguments -}
263 | DanglingBlockPrefix UnparsedBlockPrefix
264 | deriving stock (Eq, Show, Functor)
265 |
266 | data DocumentHeader a
267 | = DocumentHeader UnparsedBlockPrefix HeaderLevel a
268 | deriving stock (Eq, Show, Functor)
269 |
270 | data Document a
271 | = Document (Maybe (DocumentHeader a)) [Block a]
272 | deriving stock (Eq, Show, Functor)
273 |
274 | -- Document {
275 | -- docPrefix :: UnparsedBlockPrefix,
276 | -- docTitle :: a,
277 | -- docBlocks :: [Block UnparsedInline]
278 | -- }
279 |
280 | -- | Custom parser state for the parser for 'Block's.
281 | data State = State
282 | { -- | A stack of open 'Nestable' blocks.
283 | -- Innermost element is the top of the stack.
284 | --
285 | -- For every nestable block we store:
286 | --
287 | -- * The syntactic 'DelimiterChar' used to open the block.
288 | -- This is what we need to recognize the matching closing delimiter.
289 | -- * A stack of list item markers previously used in the current (possibly
290 | -- nested, aka multi-level, list).
291 | -- If the parser position is not currently on a list, the stack is empty.
292 | --
293 | -- The list representing the stack of open nestable blocks is non-empty: at
294 | -- the bottom of the stack there is always a value representing the
295 | -- top-level document (defined in 'State's @Monoid@ instance), so a
296 | -- one-element stack indicates no nestable block has been open.
297 | openBlocks :: NonEmpty (Marker DelimiterChar, [Marker ListChar]),
298 | -- | An environment mapping attribute names to their values (i.e. inlines).
299 | env :: Map.Map AttributeId Text
300 | }
301 | deriving stock (Eq, Show)
302 |
303 | blockParserInitialState :: State
304 | blockParserInitialState =
305 | State
306 | { -- We use @'*' :* 0@ as an arbitrary value that is always present as the
307 | -- bottom of the stack.
308 | openBlocks = (AsteriskD :* 0, []) :| [],
309 | env = mempty
310 | }
311 |
312 | type Parser m = Parsec.ParsecT [Text] State m
313 |
314 | documentP :: Monad m => Parser m (Document UnparsedInline)
315 | documentP =
316 | Document
317 | <$ option () includeP
318 | <* initialBlankLinesP
319 | <*> optional (Parsec.try documentHeaderP > "document header")
320 | <* many blankLineP
321 | <*> blocksP
322 | <* Parsec.eof
323 |
324 | documentHeaderP :: Monad m => Parser m (DocumentHeader UnparsedInline)
325 | documentHeaderP = do
326 | prefix <- option [] (NE.toList <$> blockPrefixP)
327 | (level, i) <- rawSectionHeaderP
328 | pure $ DocumentHeader prefix level i
329 |
330 | blocksP :: Monad m => Parser m [Block UnparsedInline]
331 | blocksP = many (blockP []) > "blocks"
332 |
333 | blockP :: Monad m => [LP.LineParser Text] -> Parser m (Block UnparsedInline)
334 | blockP extraParagraphFinalizers = do
335 | prefix <- option [] (NE.toList <$> blockPrefixP)
336 | blockP' prefix > "block"
337 | where
338 | blockP' prefix =
339 | (nestableP prefix > "nestable")
340 | <|> (sectionHeaderP prefix > "section header")
341 | <|> (listP prefix > "list")
342 | <|> (paragraphP prefix extraParagraphFinalizers > "paragraph")
343 | <|> (danglingBlockPrefixP prefix > "dangling block prefix")
344 |
345 | blockPrefixP :: Monad m => Parser m (NonEmpty (BlockPrefixItem UnparsedInline))
346 | blockPrefixP = some pBlockPrefixItem > "block prefix"
347 | where
348 | pBlockPrefixItem =
349 | Comment <$> blockCommentP
350 | <|> Comment <$> lineCommentSequenceP
351 | <|> attributeEntryP
352 | <|> blockIdP
353 | <|> blockAttributeListP
354 | <|> blockTitleP
355 |
356 | blockCommentP :: Monad m => Parser m Comment
357 | blockCommentP = do
358 | _ :* n <- choice $ fmap lineP' $ LP.runOfN 4 [SlashC]
359 | -- We use here an alternative version of lineP, called lineP', that does not
360 | -- try to handle pre-processor directives, as includes have no effect inside
361 | -- block comments.
362 | ts <-
363 | manyTill (lineP' LP.anyRemainder) $
364 | eitherP (lineP' (LP.count n SlashC)) Parsec.eof
365 | option () includeP
366 | _ <- many blankLineP
367 | pure $ BlockComment ts
368 | {-# ANN blockCommentP ("HLint: ignore" :: String) #-}
369 |
370 | lineCommentSequenceP :: Monad m => Parser m Comment
371 | lineCommentSequenceP =
372 | LineCommentSequence <$> some lineCommentP <* many blankLineP
373 |
374 | -- | Parses a line starting with *exactly* two '/'s.
375 | lineCommentP :: Monad m => Parser m Text
376 | lineCommentP =
377 | lineP (LP.string "//" *> Parsec.notFollowedBy (char '/') *> LP.anyRemainder)
378 |
379 | -- TODO. Add attribute continuations.
380 | attributeEntryP :: Monad m => Parser m (BlockPrefixItem UnparsedInline)
381 | attributeEntryP = attributeEntryP' <* many blankLineP
382 | where
383 | attributeEntryP' = do
384 | (k, v) <-
385 | lineP
386 | ( (,) <$ LP.char ':' <*> LP.some alphaNum
387 | <* LP.char ':'
388 | <* LP.some space <*> LP.anyRemainder
389 | )
390 | Parsec.modifyState $ \st -> st {env = Map.insert k v (env st)}
391 | pure $ AttributeEntry k $ Just (MarkupLine v :| [])
392 |
393 | blockIdP :: Monad m => Parser m (BlockPrefixItem a)
394 | blockIdP = blockIdP' <* many blankLineP
395 | where
396 | blockIdP' = MetadataItem . BlockId <$> lineP LP.blockId
397 |
398 | blockAttributeListP :: Monad m => Parser m (BlockPrefixItem a)
399 | blockAttributeListP = blockAttributeListP' <* many blankLineP
400 | where
401 | blockAttributeListP' =
402 | MetadataItem . BlockAttributeList
403 | <$> lineP LP.blockAttributeList
404 |
405 | blockTitleP :: Monad m => Parser m (BlockPrefixItem UnparsedInline)
406 | blockTitleP = blockTitleP' <* many blankLineP
407 | where
408 | blockTitleP' =
409 | MetadataItem . BlockTitle . (:| []) . MarkupLine
410 | <$> lineP (LP.char '.' *> (LP.satisfy (not . isSpace) <> LP.anyRemainder))
411 |
412 | -- | Parses a nestable delimited block.
413 | nestableP ::
414 | Monad m =>
415 | UnparsedBlockPrefix ->
416 | Parser m (Block UnparsedInline)
417 | nestableP prefix = do
418 | c <- openDelimiterP [AsteriskD, EqualsSignD]
419 | bs <- manyTill (blockP []) $ eitherP closeDelimiterP Parsec.eof
420 | _ <- many blankLineP
421 | pure $ case c of
422 | AsteriskD -> Nestable Sidebar prefix bs
423 | HyphenD -> error "nestableP: HyphenD case not implemented yet"
424 | EqualsSignD -> Nestable Example prefix bs
425 |
426 | -- | Parses a section header and computes its level.
427 | --
428 | -- __POST-CONDITION__: The computed level is greater or equal than 0.
429 | sectionHeaderP ::
430 | Monad m =>
431 | UnparsedBlockPrefix ->
432 | Parser m (Block UnparsedInline)
433 | sectionHeaderP prefix = do
434 | -- Post-condition above follows from the fact that 'LP.runOfN 1' can only
435 | -- return texts of length >= 1.
436 | -- TODO. Use type-level Nat in 'Marker', so post-condition can be checked by
437 | -- the compiler.
438 | state <- Parsec.getState
439 | case (NE.tail (openBlocks state), style) of
440 | -- If parser is currently inside a nestable block (tail state.openBlocks is
441 | -- not null), and the section header we're trying to parse has a style
442 | -- different from "discrete", this parser must fail (and the text be
443 | -- considered a regular paragraph).
444 | (_ : _, Nothing) -> empty
445 | (_ : _, Just (Last t)) | t /= "discrete" -> empty
446 | -- In any other case: parse as a section header.
447 | _ -> do
448 | (level, text) <- rawSectionHeaderP
449 | _ <- many blankLineP
450 | pure $ SectionHeader prefix level text
451 | where
452 | style = metadataStyle $ toMetadata @_ @UnparsedInline $ prefix
453 |
454 | rawSectionHeaderP :: Monad m => Parser m (HeaderLevel, UnparsedInline)
455 | rawSectionHeaderP =
456 | (\(_c :* n, x) -> (n - 1, MarkupLine x :| []))
457 | <$> lineP
458 | ( (,)
459 | <$> choice (LP.runOfN 1 [EqualsSignH]) <* some space
460 | <*> (LP.satisfy (not . isSpace) <> LP.anyRemainder)
461 | )
462 |
463 | listP ::
464 | (Monad m) =>
465 | UnparsedBlockPrefix ->
466 | Parser m (Block UnparsedInline)
467 | listP prefix =
468 | listP' prefix <* many blankLineP
469 | where
470 | allUnorderedMarkers = LP.runOfN 1 [AsteriskL, HyphenL]
471 | listP' prefix' = do
472 | state <- Parsec.getState
473 | let allowedMarkers = allUnorderedMarkers
474 | -- Disallow as markers those markers already in use in the current
475 | -- list tree of the innermost open block
476 | disallowedMarkers = snd currentBlock
477 | (currentBlock, otherBlocks) = NE.head &&& NE.tail $ openBlocks state
478 | -- Accept item with a new marker
479 | (marker@(c :* n), firstLine) <-
480 | itemFirstLineP allowedMarkers disallowedMarkers
481 | -- Add new marker to the state
482 | Parsec.setState $
483 | state
484 | { openBlocks =
485 | (fst currentBlock, marker : disallowedMarkers) :| otherBlocks
486 | }
487 | -- Complete the first item, using the already parsed first line
488 | firstItem <-
489 | itemP firstLine
490 | > "first item " <> T.unpack (fromMarker marker)
491 | -- Accept items with the same marker of the first item
492 | nextItems <-
493 | many
494 | ( itemFirstLineP [LP.count n c] []
495 | >>= itemP . snd > "item " <> T.unpack (fromMarker marker)
496 | )
497 | -- Recover state present at the beginning of the function. Functions like
498 | -- pItem could have modified it.
499 | Parsec.setState state
500 | pure $ List (Unordered Nothing) prefix' (firstItem :| nextItems)
501 | itemFirstLineP =
502 | \x -> lineP . itemFirstLine x
503 | itemFirstLine ::
504 | [LP.LineParser (Marker ListChar)] ->
505 | [Marker ListChar] ->
506 | LP.LineParser (Marker ListChar, Text)
507 | itemFirstLine allowedMarkers disallowedMarkers = do
508 | _ <- many space
509 | marker <- choice allowedMarkers
510 | if marker `elem` disallowedMarkers
511 | then empty
512 | else do
513 | _ <- some space
514 | remainder <- LP.satisfy (not . isSpace) <> LP.anyRemainder
515 | pure (marker, remainder)
516 | itemP firstLine = do
517 | -- As we are inside a list, any list marker is a finalizer of the current
518 | -- item (no blank line needed)
519 | nextLines <-
520 | many $
521 | paragraphContinuationP [snd <$> itemFirstLine allUnorderedMarkers []]
522 | nextBlocks <-
523 | option
524 | []
525 | ( ((: []) <$> sublistP > "sublist")
526 | <|> catMaybes <$> many (listContinuationP > "list continuation")
527 | > "next blocks"
528 | )
529 | _ <- many blankLineP
530 | pure $ Paragraph [] (MarkupLine firstLine :| nextLines) :| nextBlocks
531 | -- __Divergence DVB001 from Asciidoctor__. Before sublist:
532 | --
533 | -- * Full prefix (including attributes and block title) is allowed.
534 | --
535 | -- * Any number of blank lines is allowed.
536 | --
537 | -- Probably a linter should warn against any block prefix not preceded by
538 | -- blank lines.
539 | sublistP = Parsec.try $ do
540 | _ <- many blankLineP
541 | prefix' <- option [] (NE.toList <$> blockPrefixP)
542 | listP prefix'
543 | -- __Divergence DVB002 from Asciidoctor__: As in classic AsciiDoc, no blank
544 | -- lines are allowed before the @+@ sign.
545 | listContinuationP :: Monad m => Parser m (Maybe (Block UnparsedInline))
546 | listContinuationP =
547 | lineP (LP.char '+')
548 | *> optional blankLineP
549 | *> optional (blockP [snd <$> itemFirstLine allUnorderedMarkers []])
550 |
551 | paragraphP ::
552 | Monad m =>
553 | UnparsedBlockPrefix ->
554 | [LP.LineParser Text] ->
555 | Parser m (Block UnparsedInline)
556 | paragraphP prefix extraFinalizers =
557 | Paragraph prefix <$> paragraphP' <* many blankLineP
558 | where
559 | paragraphP' =
560 | (:|) <$> firstP <*> many (paragraphContinuationP extraFinalizers > "paragraph continuation")
561 | firstP :: Monad m => Parser m InputLine
562 | firstP =
563 | MarkupLine
564 | <$> lineNoneOfP
565 | -- Nestable
566 | ( (fmap fromMarker <$> LP.runOfN 4 [AsteriskD, EqualsSignD])
567 | <> [
568 | -- Blank line
569 | pure ""
570 | ]
571 | )
572 |
573 | -- Line comments (but not block comments!) can be contained in a paragraph.
574 | paragraphContinuationP :: Monad m => [LP.LineParser Text] -> Parser m InputLine
575 | paragraphContinuationP extraFinalizers =
576 | CommentLine <$> lineCommentP
577 | <|> MarkupLine
578 | <$> lineNoneOfP
579 | ( fmap Parsec.try extraFinalizers
580 | -- Nestable
581 | <> (fmap fromMarker <$> LP.runOfN 4 [AsteriskD, EqualsSignD])
582 | -- BlockComment
583 | <> (fmap fromMarker <$> LP.runOfN 4 [SlashC])
584 | <> [
585 | -- BlockId, starts with "[["
586 | Parsec.try LP.blockId,
587 | -- BlockAttributeList, starts with "["
588 | "" <$ LP.blockAttributeList,
589 | -- New block introducer, '+'
590 | Parsec.try (LP.char '+'),
591 | -- BlankLine
592 | pure ""
593 | ]
594 | )
595 |
596 | danglingBlockPrefixP ::
597 | Monad m =>
598 | UnparsedBlockPrefix ->
599 | Parser m (Block UnparsedInline)
600 | danglingBlockPrefixP [] = empty
601 | danglingBlockPrefixP prefix =
602 | DanglingBlockPrefix prefix
603 | <$ Parsec.lookAhead (closeDelimiterP <|> Parsec.eof)
604 |
605 | initialBlankLinesP :: Monad m => Parser m [Text]
606 | initialBlankLinesP = many blankLineP
607 |
608 | blankLineP :: Monad m => Parser m Text
609 | blankLineP = lineP $ pure ""
610 |
611 | -- | Argument can be a parser for the beginning of the line. Function checks
612 | -- that the part of the line not parsed is whitespace.
613 | --
614 | -- If the line is parsed successfully, this combinator checks if an include line
615 | -- follows. If that is the case it inserts the corresponding lines into the
616 | -- input stream of the parser.
617 | lineP :: Monad m => LP.LineParser a -> Parser m a
618 | lineP p = do
619 | result <- lineP' p
620 | option () includeP
621 | pure result
622 |
623 | -- | A version of 'lineP' that does not check if the line is followed by an
624 | -- include.
625 | lineP' :: Monad m => LP.LineParser a -> Parser m a
626 | lineP' p = satisfyToken $
627 | \t -> f $ Parsec.parse (p <* many space <* Parsec.eof) "" t
628 | where
629 | f (Right l) = Just l
630 | f (Left _) = Nothing
631 |
632 | -- | @lineOneOfP ps@ accepts any line that consists in syntax described by any
633 | -- parser in @ps@ plus optional space characters.
634 | --
635 | -- This function runs parsers in @ps@ in sequence, with no lookahead. This means
636 | -- that the order in which parsers appear in @ps@ is relevant, and that
637 | -- 'Parsec.try' could be needed in some elements of @ps@ if their recognized
638 | -- languages share some prefix.
639 | --
640 | -- If blank lines need to be accepted, add @pure ""@ as the last element of
641 | -- @ps@.
642 | lineOneOfP :: Monad m => [LP.LineParser a] -> Parser m a
643 | lineOneOfP parsers = do
644 | result <- lineOneOfP'
645 | option () includeP
646 | pure result
647 | where
648 | lineOneOfP' = satisfyToken $
649 | \t ->
650 | f $
651 | Parsec.parse (choice parsers <* many space <* Parsec.eof) "" t
652 | f (Right l) = Just l
653 | f (Left _) = Nothing
654 |
655 | -- | @lineNoneOfP ps@ accepts any line that does not consist in syntax described
656 | -- by any parser in @ps@ plus optional space characters.
657 | --
658 | -- This function runs parsers in @ps@ in sequence, with no lookahead. This means
659 | -- that the order in which parsers appear in @ps@ is relevant, and that
660 | -- 'Parsec.try' could be needed in some elements of @ps@ if their recognized
661 | -- languages share some prefix.
662 | --
663 | -- If blank lines need to excluded from acceptance, add @pure ""@ as the last
664 | -- element of @ps@.
665 | lineNoneOfP :: Monad m => [LP.LineParser a] -> Parser m Text
666 | lineNoneOfP parsers = do
667 | result <- lineNoneOfP'
668 | option () includeP
669 | pure result
670 | where
671 | lineNoneOfP' = satisfyToken $
672 | \t ->
673 | f t $
674 | Parsec.parse (choice parsers <* many space <* Parsec.eof) "" t
675 | f _ (Right _) = Nothing
676 | f t (Left _) = Just t
677 |
678 | includeP :: Parser m ()
679 | includeP = empty
680 |
681 | -- includeP = do
682 | -- (filename, arguments) <-
683 | -- lineP' $
684 | -- (,)
685 | -- <$ LP.string "include::"
686 | -- <*> LP.many (satisfy (/= '[')) <* char '['
687 | -- <*> LP.many (satisfy (/= ']')) <* char ']'
688 | -- current <- Parsec.getInput
689 | -- -- TODO. Read actual file content, this is a stub.
690 | -- Parsec.setInput $ ["// (STUB) include::" <> filename <> "[" <> arguments <> "]"] <> current
691 | -- -- Recursive call to handle the case in which the first line of the included
692 | -- -- file is also an include.
693 | -- option () includeP
694 |
695 | openDelimiterP ::
696 | Monad m =>
697 | [SpecialChar DelimiterChar] ->
698 | Parser m (SpecialChar DelimiterChar)
699 | openDelimiterP cs = do
700 | -- Parsec.lookAhead needed here because in case we fail later on (because the
701 | -- block is already open) we don't want to consume any input.
702 | (c :* n) <- Parsec.lookAhead $ Parsec.try $ lineOneOfP (LP.runOfN 4 cs)
703 | st <- Parsec.getState
704 | -- If block is already open (the delimiter is in the stack of open blocks),
705 | -- we're not opening it again, but fail. In case we don't fail, we consume the
706 | -- line that was looked ahead above.
707 | if (c :* n) `elem` (fst <$> openBlocks st)
708 | then empty
709 | else
710 | ( do
711 | -- Add found delimiter to the stack of open blocks
712 | Parsec.putState (st {openBlocks = (c :* n, []) <| openBlocks st})
713 | -- Complete consumption of the token (aka one line of input), and
714 | -- following blanklines
715 | _ <- lineP LP.anyRemainder
716 | _ <- many blankLineP
717 | pure c
718 | )
719 |
720 | closeDelimiterP :: Monad m => Parser m ()
721 | closeDelimiterP = do
722 | st <- Parsec.getState
723 | let (c :* n, _) = NE.head (openBlocks st)
724 | case NE.tail (openBlocks st) of
725 | -- In presence of DanglingBlockPrefix'es, we can try to pop from an
726 | -- openBlocks stack that contains the initial open block only. We do nothing
727 | -- in this case.
728 | [] -> pure ()
729 | b : bs -> do
730 | -- If c :* n found in openBlocks stack, pop one element. Only consume line
731 | -- from input (and look for includes) if the found delimiter matches
732 | -- openBlocks' top.
733 | _ <-
734 | lineP (LP.count n c)
735 | <|> Parsec.lookAhead
736 | ( choice $
737 | fmap (\(c' :* n', _) -> lineP' (LP.count n' c')) (b : bs)
738 | )
739 | Parsec.putState $ st {openBlocks = b :| bs}
740 |
741 | -- TODO: Add name to source positions (possibly storing current filename when an
742 | -- inline arrives).
743 | --
744 | -- TODO: Fix line numbering in the presence of includes.
745 | satisfyToken :: Monad m => (Text -> Maybe a) -> Parser m a
746 | satisfyToken matcher = Parsec.tokenPrim show updatePos matcher
747 | where
748 | updatePos :: Parsec.SourcePos -> Text -> [Text] -> Parsec.SourcePos
749 | updatePos pos _ _ = Parsec.incSourceLine pos 1
750 | {-# ANN satisfyToken ("HLint: ignore" :: String) #-}
751 |
--------------------------------------------------------------------------------
/test/Tests/Inlines.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 |
3 | module Tests.Inlines
4 | ( inlineUnitTests,
5 | )
6 | where
7 |
8 | import Data.Either (isLeft)
9 | import Data.Functor.Identity (Identity)
10 | import Data.List.NonEmpty (NonEmpty (..))
11 | import Data.Map.Internal (Identity (runIdentity))
12 | import Data.Text (Text)
13 | import Test.Hspec.Expectations.Pretty (shouldBe)
14 | import Test.Tasty (TestTree, testGroup)
15 | import Test.Tasty.HUnit (assertBool, assertFailure, testCase)
16 | import Text.AsciiDoc.Inlines
17 | import qualified Text.Parsec as Parsec
18 |
19 | parseInline :: Text -> IO Inline
20 | parseInline t =
21 | case parseTest inlinesP t of
22 | Right result -> pure result
23 | Left parseError -> assertFailure $ "Parser fails: " <> show parseError
24 |
25 | parseTest :: Parser Identity a -> Text -> Either Parsec.ParseError a
26 | parseTest parser t =
27 | runIdentity $ Parsec.runParserT parser inlineParserInitialState "" t
28 |
29 | inlineUnitTests :: TestTree
30 | inlineUnitTests =
31 | testGroup
32 | "inline unit tests"
33 | [ simpleInlineTests,
34 | boldInlineTests,
35 | unconstrainedStylingTests,
36 | mixedFormattingStyleTests,
37 | enclosureInlineAttributeListTests,
38 | punctuationSymbolTests
39 | ]
40 |
41 | simpleInlineTests :: TestTree
42 | simpleInlineTests =
43 | testGroup
44 | "simple inlines"
45 | [ testCase "single-line, no formatting marks" $ do
46 | i <- parseInline "some words with no format"
47 | i `shouldBe` InlineSeq (AlphaNum "some" :| [Space " ", AlphaNum "words", Space " ", AlphaNum "with", Space " ", AlphaNum "no", Space " ", AlphaNum "format"]),
48 | testCase "Space at the beginning" $
49 | assertBool "Parser doesn't fail" $
50 | isLeft $
51 | parseTest inlinesP " some words preceded by space",
52 | testCase "no formatting marks with space at the end" $ do
53 | i <- parseInline "some words with no format "
54 | i `shouldBe` InlineSeq (AlphaNum "some" :| [Space " ", AlphaNum "words", Space " ", AlphaNum "with", Space " ", AlphaNum "no", Space " ", AlphaNum "format", Space " "])
55 | ]
56 |
57 | boldInlineTests :: TestTree
58 | boldInlineTests =
59 | testGroup
60 | "bold inlines"
61 | [ testCase "single-line, bold string" $ do
62 | i <- parseInline "*a sentence all in strong*"
63 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (AlphaNum "a" :| [Space " ", AlphaNum "sentence", Space " ", AlphaNum "all", Space " ", AlphaNum "in", Space " ", AlphaNum "strong"]) "*" :| []),
64 | testCase "single-line, bold string with space at the end" $ do
65 | i <- parseInline "*a sentence all in strong* "
66 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (AlphaNum "a" :| [Space " ", AlphaNum "sentence", Space " ", AlphaNum "all", Space " ", AlphaNum "in", Space " ", AlphaNum "strong"]) "*" :| [Space " "]),
67 | testCase "a word in bold in the middle" $ do
68 | i <- parseInline "a *few* words"
69 | i `shouldBe` InlineSeq (AlphaNum "a" :| [Space " ", StyledText Bold defaultAttributeList "*" (AlphaNum "few" :| []) "*", Space " ", AlphaNum "words"]),
70 | testCase "two words in bold at the beginning" $ do
71 | i <- parseInline "*a few* words"
72 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (AlphaNum "a" :| [Space " ", AlphaNum "few"]) "*" :| [Space " ", AlphaNum "words"]),
73 | testCase "two words in bold at the end" $ do
74 | i <- parseInline "a *few words*"
75 | i `shouldBe` InlineSeq (AlphaNum "a" :| [Space " ", StyledText Bold defaultAttributeList "*" (AlphaNum "few" :| [Space " ", AlphaNum "words"]) "*"]),
76 | testCase "bad bold ending with closing mark after space" $ do
77 | i <- parseInline "*a few *"
78 | i `shouldBe` InlineSeq (Symbol "*" :| [AlphaNum "a", Space " ", AlphaNum "few", Space " ", Symbol "*"]),
79 | testCase "bad bold ending with closing mark after space and before word" $ do
80 | i <- parseInline "*a *few words"
81 | i `shouldBe` InlineSeq (Symbol "*" :| [AlphaNum "a", Space " ", Symbol "*", AlphaNum "few", Space " ", AlphaNum "words"]),
82 | testCase "asterisk in the middle of bold phrase" $ do
83 | i <- parseInline "*a *few words*"
84 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (AlphaNum "a" :| [Space " ", Symbol "*", AlphaNum "few", Space " ", AlphaNum "words"]) "*" :| []),
85 | testCase "single asterisk in phrase" $ do
86 | i <- parseInline "a *few words"
87 | i `shouldBe` InlineSeq (AlphaNum "a" :| [Space " ", Symbol "*", AlphaNum "few", Space " ", AlphaNum "words"]),
88 | testCase "single asterisk in phrase with space at the end" $ do
89 | i <- parseInline "a *few words "
90 | i `shouldBe` InlineSeq (AlphaNum "a" :| [Space " ", Symbol "*", AlphaNum "few", Space " ", AlphaNum "words", Space " "]),
91 | testCase "single asterisk in the middle of a word" $ do
92 | i <- parseInline "a f*ew words"
93 | i `shouldBe` InlineSeq (AlphaNum "a" :| [Space " ", AlphaNum "f", Symbol "*", AlphaNum "ew", Space " ", AlphaNum "words"]),
94 | testCase "an asterisk in the middle of a word in strong phrase" $ do
95 | i <- parseInline "*a f*ew* words"
96 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (AlphaNum "a" :| [Space " ", AlphaNum "f", Symbol "*", AlphaNum "ew"]) "*" :| [Space " ", AlphaNum "words"]),
97 | testCase "single asterisk followed by word and space" $ do
98 | i <- parseInline "*a "
99 | i `shouldBe` InlineSeq (Symbol "*" :| [AlphaNum "a", Space " "])
100 | ]
101 |
102 | unconstrainedStylingTests :: TestTree
103 | unconstrainedStylingTests =
104 | testGroup
105 | "unconstrained formatting marks"
106 | [ testCase "nesting unconstrained inside constrained, with no space" $ do
107 | i <- parseInline "#a##b##c#"
108 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [StyledText Custom defaultAttributeList "##" (AlphaNum "b" :| []) "##", AlphaNum "c"]) "#" :| []),
109 | testCase "unpaired opening mark before correctly closed unconstrained pair" $ do
110 | i <- parseInline "#a##b##"
111 | i `shouldBe` InlineSeq (Symbol "#" :| [AlphaNum "a", StyledText Custom defaultAttributeList "##" (AlphaNum "b" :| []) "##"]),
112 | -- Divergence from Asciidoctor.
113 | testCase "(## #a ##b## c# ##)" $ do
114 | i <- parseInline "## #a ##b## c# ##"
115 | -- let alt_result = InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", StyledText Custom defaultAttributeList "##" (AlphaNum "b" :| []) "##", Space " ", AlphaNum "c"]) "#", Space " "]) "##" :| [])
116 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [Symbol "#", AlphaNum "a", Space " "]) "##" :| [AlphaNum "b", StyledText Custom defaultAttributeList "##" (Space " " :| [AlphaNum "c", Symbol "#", Space " "]) "##"])
117 | i `shouldBe` result,
118 | -- libasciidoc fails test
119 | testCase "unpaired opening mark directly inside unconstrained pair" $ do
120 | i <- parseInline "## #a b ##"
121 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [Symbol "#", AlphaNum "a", Space " ", AlphaNum "b", Space " "]) "##" :| [])
122 | i `shouldBe` result,
123 | -- Divergence DVI001 from Asciidoctor.
124 | testCase "three opening marks, and three closing marks, with no space" $ do
125 | i <- parseInline "###a###"
126 | -- Asciidoctor:
127 | -- let alt_result = InlineSeq (StyledText Custom defaultAttributeList "##" (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| []) "#" :| []) "##" :| [])
128 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Symbol "#" :| [AlphaNum "a"]) "##" :| [Symbol "#"])
129 | i `shouldBe` result,
130 | -- Divergence DVI001 from Asciidoctor.
131 | testCase "three opening marks, and three closing marks, two words and no space" $ do
132 | i <- parseInline "###a b###"
133 | -- Asciidoctor:
134 | -- let alt_result = InlineSeq (StyledText Custom defaultAttributeList "##" (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", AlphaNum "b"]) "#" :| []) "##" :| [])
135 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Symbol "#" :| [AlphaNum "a", Space " ", AlphaNum "b"]) "##" :| [Symbol "#"])
136 | i `shouldBe` result,
137 | -- Divergence from Asciidoctor.
138 | -- TODO. This one is different from DVI001. Asciidoctor's outputs is just
139 | -- wrong. It looks like it works because it doesn't respect the nesting
140 | -- rule.
141 | testCase "three opening marks, and three closing marks, two words and no space" $ do
142 | -- Asciidoctor:
143 | -- let alt_result = InlineSeq (StyledText Custom defaultAttributeList "#" (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| [Space " ", AlphaNum "b", Space " "]) "##" :| []) "#" :| [])
144 | i <- parseInline "###a b ###"
145 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Symbol "#" :| [AlphaNum "a", Space " ", AlphaNum "b", Space " "]) "##" :| [Symbol "#"])
146 | i `shouldBe` result,
147 | testCase "unconstrained styled word" $ do
148 | i <- parseInline "##a##"
149 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| []) "##" :| []),
150 | testCase "unconstrained formatting pair with inner space on the left" $ do
151 | i <- parseInline "## a##"
152 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [AlphaNum "a"]) "##" :| []),
153 | testCase "unconstrained formatting pair with inner space on the right" $ do
154 | i <- parseInline "##a ##"
155 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| [Space " "]) "##" :| []),
156 | testCase "unconstrained formatting pair with inner space on both sides" $ do
157 | i <- parseInline "## a ##"
158 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [AlphaNum "a", Space " "]) "##" :| []),
159 | testCase "unbalanced marks, one missing on the left" $ do
160 | i <- parseInline "##a#"
161 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (Symbol "#" :| [AlphaNum "a"]) "#" :| []),
162 | testCase "unbalanced marks, one missing on the right" $ do
163 | i <- parseInline "#a##"
164 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| []) "#" :| [Symbol "#"]),
165 | testCase "nesting constrained directly inside unconstrained, with space" $ do
166 | i <- parseInline "## #a# ##"
167 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| []) "#", Space " "]) "##" :| []),
168 | testCase "nesting unconstrained inside constrained, with spaces everywhere" $ do
169 | i <- parseInline "#a ## b ## c#"
170 | let result = InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", StyledText Custom defaultAttributeList "##" (Space " " :| [AlphaNum "b", Space " "]) "##", Space " ", AlphaNum "c"]) "#" :| [])
171 | i `shouldBe` result,
172 | testCase "bad nesting: constrained directly inside constrained" $ do
173 | i <- parseInline "#a #b# c#"
174 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", Symbol "#", AlphaNum "b"]) "#" :| [Space " ", AlphaNum "c", Symbol "#"]),
175 | testCase "two unconstrained pairs, false nesting" $ do
176 | i <- parseInline "##a ##b## c##"
177 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| [Space " "]) "##" :| [AlphaNum "b", StyledText Custom defaultAttributeList "##" (Space " " :| [AlphaNum "c"]) "##"]),
178 | testCase "unpaired opening mark inside unconstrained pair" $ do
179 | i <- parseInline "##a #b ##"
180 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| [Space " ", Symbol "#", AlphaNum "b", Space " "]) "##" :| [])
181 | i `shouldBe` result,
182 | testCase "unpaired closing mark inside unconstrained pair" $ do
183 | i <- parseInline "##a b# ##"
184 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| [Space " ", AlphaNum "b", Symbol "#", Space " "]) "##" :| []),
185 | testCase "unpaired mark between space inside unconstrained pair" $ do
186 | i <- parseInline "##a # ##"
187 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| [Space " ", Symbol "#", Space " "]) "##" :| []),
188 | testCase "double mark ending constrained enclosure" $ do
189 | i <- parseInline "#a ## b#"
190 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", Symbol "#"]) "#" :| [Space " ", AlphaNum "b", Symbol "#"]),
191 | testCase "nesting constrained inside unconstrained, with spaces on both sides" $ do
192 | i <- parseInline "##a #b# c##"
193 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| [Space " ", StyledText Custom defaultAttributeList "#" (AlphaNum "b" :| []) "#", Space " ", AlphaNum "c"]) "##" :| []),
194 | testCase "nesting unconstrained inside constrained, with spaces on both sides" $ do
195 | i <- parseInline "#a ##b## c#"
196 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", StyledText Custom defaultAttributeList "##" (AlphaNum "b" :| []) "##", Space " ", AlphaNum "c"]) "#" :| []),
197 | testCase "unpaired opening mark inside constrained pair" $ do
198 | i <- parseInline "#a ##b c#"
199 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", Symbol "#", Symbol "#", AlphaNum "b", Space " ", AlphaNum "c"]) "#" :| []),
200 | testCase "unpaired opening mark after correcty closed pair and some noise" $ do
201 | i <- parseInline "##a##b# ##"
202 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (AlphaNum "a" :| []) "##" :| [AlphaNum "b", Symbol "#", Space " ", Symbol "#", Symbol "#"]),
203 | testCase "three-mark sequence" $ do
204 | i <- parseInline "###"
205 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (Symbol "#" :| []) "#" :| []),
206 | testCase "four-mark sequence" $ do
207 | i <- parseInline "####"
208 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (Symbol "#" :| []) "#" :| [Symbol "#"]),
209 | testCase "five-mark sequence" $ do
210 | i <- parseInline "#####"
211 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Symbol "#" :| []) "##" :| [])
212 | i `shouldBe` result,
213 | -- Divergence DVI002 from Asciidoctor: Asciidoctor accepts nested
214 | -- enclosures with empty inline inside.
215 | testCase "six-mark sequence" $ do
216 | i <- parseInline "######"
217 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Symbol "#" :| []) "##" :| [Symbol "#"])
218 | i `shouldBe` result,
219 | testCase "unconstrained enclosure with a single space" $ do
220 | i <- parseInline "## ##"
221 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| []) "##" :| []),
222 | testCase "two three-mark sequences separated by space" $ do
223 | i <- parseInline "### ###"
224 | -- Asciidoctor seems inconsistent w.r.t "###a###" because in this case
225 | -- it flips U and C.
226 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Symbol "#" :| [Space " "]) "##" :| [Symbol "#"])
227 | i `shouldBe` result,
228 | -- Divergence DVI003 from Asciidoctor: Asciidoctor doesn't parse "...#b
229 | -- c#" as an enclosure, but it does if the mark of both enclosures is
230 | -- different. We consider the behavior here more uniform.
231 | testCase "two constrained enclosures with the same mark an no space in between" $ do
232 | i <- parseInline "#a##b c#"
233 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| []) "#" :| [StyledText Custom defaultAttributeList "#" (AlphaNum "b" :| [Space " ", AlphaNum "c"]) "#"])
234 | ]
235 |
236 | mixedFormattingStyleTests :: TestTree
237 | mixedFormattingStyleTests =
238 | testGroup
239 | "mixed formatting styles"
240 | [ testCase "constrained monospace inside italics, inside bold, no space" $ do
241 | i <- parseInline "*_`a`_*"
242 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (StyledText Italic defaultAttributeList "_" (StyledText Monospace defaultAttributeList "`" (AlphaNum "a" :| []) "`" :| []) "_" :| []) "*" :| []),
243 | testCase "constrained monospace inside unconstrained italics, inside bold, no space" $ do
244 | i <- parseInline "*__`a`__*"
245 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (StyledText Italic defaultAttributeList "__" (StyledText Monospace defaultAttributeList "`" (AlphaNum "a" :| []) "`" :| []) "__" :| []) "*" :| []),
246 | testCase "constrained monospace inside unconstrained italics, inside bold, some space" $ do
247 | i <- parseInline "*a __ `b` __c*"
248 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "*" (AlphaNum "a" :| [Space " ", StyledText Italic defaultAttributeList "__" (Space " " :| [StyledText Monospace defaultAttributeList "`" (AlphaNum "b" :| []) "`", Space " "]) "__", AlphaNum "c"]) "*" :| []),
249 | testCase "constrained italics interrupted inside custom" $ do
250 | i <- parseInline "#a _b#"
251 | i `shouldBe` InlineSeq (StyledText Custom defaultAttributeList "#" (AlphaNum "a" :| [Space " ", Symbol "_", AlphaNum "b"]) "#" :| []),
252 | -- Divergence from Asciidoctor: Asciidoctor doesn't respect nesting rule.
253 | testCase "(## _a ##b## c_ ##)" $ do
254 | i <- parseInline "## _a ##b## c_ ##"
255 | -- let alt_result = InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [StyledText Italic defaultAttributeList "_" (AlphaNum "a" :| [Space " ", StyledText Custom defaultAttributeList "##" (AlphaNum "b" :| []) "##", Space " ", AlphaNum "c"]) "_", Space " "]) "##" :| [])
256 | let result = InlineSeq (StyledText Custom defaultAttributeList "##" (Space " " :| [Symbol "_", AlphaNum "a", Space " "]) "##" :| [AlphaNum "b", StyledText Custom defaultAttributeList "##" (Space " " :| [AlphaNum "c", Symbol "_", Space " "]) "##"])
257 | i `shouldBe` result,
258 | testCase "(__ #a##b##c# __)" $ do
259 | i <- parseInline "__ #a##b##c# __"
260 | let result = InlineSeq (StyledText Italic (InlineAttributeList "") "__" (Space " " :| [StyledText Custom (InlineAttributeList "") "#" (AlphaNum "a" :| [StyledText Custom (InlineAttributeList "") "##" (AlphaNum "b" :| []) "##", AlphaNum "c"]) "#", Space " "]) "__" :| [])
261 | i `shouldBe` result,
262 | testCase "(#a *##b##*#)" $ do
263 | i <- parseInline "#a *##b##*#"
264 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "#" (AlphaNum "a" :| [Space " ", StyledText Bold (InlineAttributeList "") "*" (StyledText Custom (InlineAttributeList "") "##" (AlphaNum "b" :| []) "##" :| []) "*"]) "#" :| [])
265 | i `shouldBe` result,
266 | -- Divergence from Asciidoctor: Asciidoctor doesn't respect nesting rule.
267 | testCase "(*a _b* c_)" $ do
268 | i <- parseInline "*a _b* c_"
269 | -- let alt_result = InlineSeq (Symbol "*" :| [AlphaNum "a", Space " ", StyledText Italic defaultAttributeList "_" (AlphaNum "b" :| [Symbol "*", Space " ", AlphaNum "c"]) "_"])
270 | let result = InlineSeq (StyledText Bold defaultAttributeList "*" (AlphaNum "a" :| [Space " ", Symbol "_", AlphaNum "b"]) "*" :| [Space " ", AlphaNum "c", Symbol "_"])
271 | i `shouldBe` result,
272 | -- Divergence from Asciidoctor: Asciidoctor doesn't respect nesting rule.
273 | testCase "(**a __b** c__)" $ do
274 | i <- parseInline "**a __b** c__"
275 | -- let alt_result = InlineSeq (Symbol "*" :| [Symbol "*", AlphaNum "a", Space " ", StyledText Italic defaultAttributeList "__" (AlphaNum "b" :| [Symbol "*", Symbol "*", Space " ", AlphaNum "c"]) "__"])
276 | let result = InlineSeq (StyledText Bold defaultAttributeList "**" (AlphaNum "a" :| [Space " ", Symbol "_", Symbol "_", AlphaNum "b"]) "**" :| [Space " ", AlphaNum "c", Symbol "_", Symbol "_"])
277 | i `shouldBe` result,
278 | -- Divergence from Asciidoctor: Asciidoctor doesn't respect nesting rule.
279 | testCase "(**a _b** c_)" $ do
280 | i <- parseInline "**a _b** c_"
281 | i `shouldBe` InlineSeq (StyledText Bold defaultAttributeList "**" (AlphaNum "a" :| [Space " ", Symbol "_", AlphaNum "b"]) "**" :| [Space " ", AlphaNum "c", Symbol "_"]),
282 | -- Divergence from Asciidoctor: Asciidoctor doesn't respect nesting rule.
283 | testCase "(*a __b* c__)" $ do
284 | i <- parseInline "*a __b* c__"
285 | i `shouldBe` InlineSeq (Symbol "*" :| [AlphaNum "a", Space " ", StyledText Italic defaultAttributeList "__" (AlphaNum "b" :| [Symbol "*", Space " ", AlphaNum "c"]) "__"]),
286 | testCase "(#*a _b* c_#)" $ do
287 | i <- parseInline "#*a _b* c_#"
288 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "#" (StyledText Bold (InlineAttributeList "") "*" (AlphaNum "a" :| [Space " ", Symbol "_", AlphaNum "b"]) "*" :| [Space " ", AlphaNum "c", Symbol "_"]) "#" :| [])
289 | i `shouldBe` result,
290 | testCase "(#**a __b** c__#)" $ do
291 | i <- parseInline "#**a __b** c__#"
292 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "#" (StyledText Bold (InlineAttributeList "") "**" (AlphaNum "a" :| [Space " ", Symbol "_", Symbol "_", AlphaNum "b"]) "**" :| [Space " ", AlphaNum "c", Symbol "_", Symbol "_"]) "#" :| [])
293 | i `shouldBe` result,
294 | testCase "(#**a _b** c_#)" $ do
295 | i <- parseInline "#**a _b** c_#"
296 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "#" (StyledText Bold (InlineAttributeList "") "**" (AlphaNum "a" :| [Space " ", Symbol "_", AlphaNum "b"]) "**" :| [Space " ", AlphaNum "c", Symbol "_"]) "#" :| [])
297 | i `shouldBe` result,
298 | testCase "(#*a __b* c__#)" $ do
299 | i <- parseInline "#*a __b* c__#"
300 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "#" (Symbol "*" :| [AlphaNum "a", Space " ", StyledText Italic (InlineAttributeList "") "__" (AlphaNum "b" :| [Symbol "*", Space " ", AlphaNum "c"]) "__"]) "#" :| [])
301 | i `shouldBe` result,
302 | testCase "(##*a _b* c_##)" $ do
303 | i <- parseInline "##*a _b* c_##"
304 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (StyledText Bold (InlineAttributeList "") "*" (AlphaNum "a" :| [Space " ", Symbol "_", AlphaNum "b"]) "*" :| [Space " ", AlphaNum "c", Symbol "_"]) "##" :| [])
305 | i `shouldBe` result,
306 | testCase "(##**a __b** c__##)" $ do
307 | i <- parseInline "##**a __b** c__##"
308 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (StyledText Bold (InlineAttributeList "") "**" (AlphaNum "a" :| [Space " ", Symbol "_", Symbol "_", AlphaNum "b"]) "**" :| [Space " ", AlphaNum "c", Symbol "_", Symbol "_"]) "##" :| [])
309 | i `shouldBe` result,
310 | testCase "(##**a _b** c_##)" $ do
311 | i <- parseInline "##**a _b** c_##"
312 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (StyledText Bold (InlineAttributeList "") "**" (AlphaNum "a" :| [Space " ", Symbol "_", AlphaNum "b"]) "**" :| [Space " ", AlphaNum "c", Symbol "_"]) "##" :| [])
313 | i `shouldBe` result,
314 | testCase "(##*a __b* c__##)" $ do
315 | i <- parseInline "##*a __b* c__##"
316 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (Symbol "*" :| [AlphaNum "a", Space " ", StyledText Italic (InlineAttributeList "") "__" (AlphaNum "b" :| [Symbol "*", Space " ", AlphaNum "c"]) "__"]) "##" :| [])
317 | i `shouldBe` result,
318 | testCase "(##*a _#b c#* d_##)" $ do
319 | i <- parseInline "##*a _#b c#* d_##"
320 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (StyledText Bold (InlineAttributeList "") "*" (AlphaNum "a" :| [Space " ", Symbol "_", StyledText Custom (InlineAttributeList "") "#" (AlphaNum "b" :| [Space " ", AlphaNum "c"]) "#"]) "*" :| [Space " ", AlphaNum "d", Symbol "_"]) "##" :| [])
321 | i `shouldBe` result,
322 | testCase "(##**a __#b c#** d__##)" $ do
323 | i <- parseInline "##**a __#b c#** d__##"
324 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (StyledText Bold (InlineAttributeList "") "**" (AlphaNum "a" :| [Space " ", Symbol "_", Symbol "_", StyledText Custom (InlineAttributeList "") "#" (AlphaNum "b" :| [Space " ", AlphaNum "c"]) "#"]) "**" :| [Space " ", AlphaNum "d", Symbol "_", Symbol "_"]) "##" :| [])
325 | i `shouldBe` result,
326 | testCase "(##**a _#b c#** d_##)" $ do
327 | i <- parseInline "##**a _#b c#** d_##"
328 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (StyledText Bold (InlineAttributeList "") "**" (AlphaNum "a" :| [Space " ", Symbol "_", StyledText Custom (InlineAttributeList "") "#" (AlphaNum "b" :| [Space " ", AlphaNum "c"]) "#"]) "**" :| [Space " ", AlphaNum "d", Symbol "_"]) "##" :| [])
329 | i `shouldBe` result,
330 | testCase "(##*a __#b c#* d__##)" $ do
331 | i <- parseInline "##*a __#b c#* d__##"
332 | let result = InlineSeq (StyledText Custom (InlineAttributeList "") "##" (Symbol "*" :| [AlphaNum "a", Space " ", StyledText Italic (InlineAttributeList "") "__" (StyledText Custom (InlineAttributeList "") "#" (AlphaNum "b" :| [Space " ", AlphaNum "c"]) "#" :| [Symbol "*", Space " ", AlphaNum "d"]) "__"]) "##" :| [])
333 | i `shouldBe` result
334 | ]
335 |
336 | enclosureInlineAttributeListTests :: TestTree
337 | enclosureInlineAttributeListTests =
338 | testGroup
339 | "parameter lists for enclosures"
340 | [ testCase "simple parameter list for constrained enclosure" $ do
341 | i <- parseInline "[underline]#a#"
342 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "underline") "#" (AlphaNum "a" :| []) "#" :| []),
343 | testCase "simple parameter list for unconstrained enclosure" $ do
344 | i <- parseInline "[underline]##a##"
345 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "underline") "##" (AlphaNum "a" :| []) "##" :| []),
346 | testCase "parameter list for unconstrained enclosure preceded by word" $ do
347 | i <- parseInline "a[underline]##b##"
348 | i `shouldBe` InlineSeq (AlphaNum "a" :| [StyledText Custom (InlineAttributeList "underline") "##" (AlphaNum "b" :| []) "##"]),
349 | testCase "parameter list for unconstrained enclosure followed by word" $ do
350 | i <- parseInline "[underline]##a##b"
351 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "underline") "##" (AlphaNum "a" :| []) "##" :| [AlphaNum "b"]),
352 | testCase "failed parameter list for constrained enclosure preceded by word" $ do
353 | i <- parseInline "a[underline]#b#"
354 | i `shouldBe` InlineSeq (AlphaNum "a" :| [Symbol "[", AlphaNum "underline", Symbol "]", StyledText Custom (InlineAttributeList "") "#" (AlphaNum "b" :| []) "#"]),
355 | testCase "failed parameter list for constrained enclosure followed by word" $ do
356 | i <- parseInline "[underline]#a#b"
357 | i `shouldBe` InlineSeq (Symbol "[" :| [AlphaNum "underline", Symbol "]", Symbol "#", AlphaNum "a", Symbol "#", AlphaNum "b"]),
358 | testCase "unfinished (failed) parameter list" $ do
359 | i <- parseInline "[underline"
360 | i `shouldBe` InlineSeq (Symbol "[" :| [AlphaNum "underline"]),
361 | testCase "isolated (failed) parameter list" $ do
362 | i <- parseInline "[underline] #a#"
363 | i `shouldBe` InlineSeq (Symbol "[" :| [AlphaNum "underline", Symbol "]", Space " ", StyledText Custom (InlineAttributeList "") "#" (AlphaNum "a" :| []) "#"])
364 | ]
365 |
366 | punctuationSymbolTests :: TestTree
367 | punctuationSymbolTests =
368 | testGroup
369 | "punctuation symbols"
370 | [ testCase "(a.) sentence finalizer" $ do
371 | i <- parseInline "a."
372 | i `shouldBe` InlineSeq (AlphaNum "a" :| [Symbol "."]),
373 | testCase "(#a.#) sentence finalizer at end of constrained enclosure" $ do
374 | i <- parseInline "#a.#"
375 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "") "#" (AlphaNum "a" :| [Symbol "."]) "#" :| []),
376 | testCase "(##a.##) sentence finalizer at end of unconstrained enclosure" $ do
377 | i <- parseInline "##a.##"
378 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "") "##" (AlphaNum "a" :| [Symbol "."]) "##" :| []),
379 | testCase "(*#a#, b*) comma after enclosure, nested inside another enclosure" $ do
380 | i <- parseInline "*#a#, b*"
381 | i `shouldBe` InlineSeq (StyledText Bold (InlineAttributeList "") "*" (StyledText Custom (InlineAttributeList "") "#" (AlphaNum "a" :| []) "#" :| [Symbol ",", Space " ", AlphaNum "b"]) "*" :| []),
382 | testCase "(#a#,#b#) punctuation symbol separating two enclosures" $ do
383 | i <- parseInline "#a#,#b#"
384 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "") "#" (AlphaNum "a" :| []) "#" :| [Symbol ",", StyledText Custom (InlineAttributeList "") "#" (AlphaNum "b" :| []) "#"]),
385 | testCase "(#¿a?#) question marks inside constrained enclosure" $ do
386 | i <- parseInline "#¿a?#"
387 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "") "#" (Symbol "¿" :| [AlphaNum "a", Symbol "?"]) "#" :| []),
388 | testCase "(##¿a?##) question marks inside unconstrained enclosure" $ do
389 | i <- parseInline "##¿a?##"
390 | i `shouldBe` InlineSeq (StyledText Custom (InlineAttributeList "") "##" (Symbol "¿" :| [AlphaNum "a", Symbol "?"]) "##" :| []),
391 | testCase "(¿#a#?) question marks outside constrained enclosure" $ do
392 | i <- parseInline "¿#a#?"
393 | i `shouldBe` InlineSeq (Symbol "¿" :| [StyledText Custom (InlineAttributeList "") "#" (AlphaNum "a" :| []) "#", Symbol "?"]),
394 | testCase "(¿##a##?) question marks outside unconstrained enclosure" $ do
395 | i <- parseInline "¿##a##?"
396 | i `shouldBe` InlineSeq (Symbol "¿" :| [StyledText Custom (InlineAttributeList "") "##" (AlphaNum "a" :| []) "##", Symbol "?"])
397 | ]
398 |
--------------------------------------------------------------------------------
/test/Tests/Blocks.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE OverloadedStrings #-}
2 |
3 | module Tests.Blocks
4 | ( parseTest,
5 | blockUnitTests,
6 | )
7 | where
8 |
9 | import Data.Functor.Identity (Identity (runIdentity))
10 | import Data.List.NonEmpty (NonEmpty ((:|)))
11 | import Data.Semigroup (Last (..))
12 | import Data.Text (Text)
13 | import Test.Hspec.Expectations.Pretty (shouldBe)
14 | import Test.Tasty (TestTree, testGroup)
15 | import Test.Tasty.HUnit (assertFailure, testCase)
16 | import Text.AsciiDoc.Blocks
17 | import Text.AsciiDoc.Metadata
18 | import Text.AsciiDoc.UnparsedInline
19 | import qualified Text.Parsec as Parsec
20 |
21 | parseDocument :: [Text] -> IO (Document UnparsedInline)
22 | parseDocument t = case parseTest documentP t of
23 | Right x -> pure x
24 | Left parseError -> assertFailure $ "Parser fails: " <> show parseError
25 |
26 | parseBlocks :: [Text] -> IO [Block UnparsedInline]
27 | parseBlocks t = case parseTest blocksP t of
28 | Right x -> pure x
29 | Left parseError -> assertFailure $ "Parser fails: " <> show parseError
30 |
31 | parseTest :: Parser Identity a -> [Text] -> Either Parsec.ParseError a
32 | parseTest parser tokens =
33 | runIdentity $ Parsec.runParserT parser blockParserInitialState "" tokens
34 |
35 | blockUnitTests :: TestTree
36 | blockUnitTests =
37 | testGroup
38 | "block unit tests"
39 | [ documentUnitTests,
40 | blockCornerCaseUnitTests,
41 | paragraphUnitTests,
42 | sectionHeaderUnitTests,
43 | danglingBlockPrefixUnitTests,
44 | nestableUnitTests,
45 | unorderedListUnitTests,
46 | nestedListsUnitTests,
47 | listContinuationUnitTests,
48 | commentUnitTests
49 | ]
50 |
51 | documentUnitTests :: TestTree
52 | documentUnitTests =
53 | testGroup
54 | "document unit tests"
55 | [ testCase "empty document" $ do
56 | p <-
57 | parseDocument
58 | []
59 | p `shouldBe` Document Nothing [],
60 | testCase "single document title" $ do
61 | p <-
62 | parseDocument
63 | [ "= Foo"
64 | ]
65 | p
66 | `shouldBe` Document
67 | (Just (DocumentHeader [] 0 (MarkupLine "Foo" :| [])))
68 | [],
69 | testCase "single document title with prefix" $ do
70 | p <-
71 | parseDocument
72 | [ "[[Foo]]",
73 | "= Bar"
74 | ]
75 | p
76 | `shouldBe` Document
77 | (Just (DocumentHeader [MetadataItem (BlockId "Foo")] 0 (MarkupLine "Bar" :| [])))
78 | [],
79 | testCase "document title with prefix followed by paragraph" $ do
80 | p <-
81 | parseDocument
82 | [ "[[Foo]]",
83 | "= Bar",
84 | "Baz"
85 | ]
86 | p
87 | `shouldBe` Document
88 | (Just (DocumentHeader [MetadataItem (BlockId "Foo")] 0 (MarkupLine "Bar" :| [])))
89 | [Paragraph [] (MarkupLine "Baz" :| [])],
90 | testCase "document title followed by empty line and paragraph" $ do
91 | p <-
92 | parseDocument
93 | [ "= Foo",
94 | "",
95 | "Bar"
96 | ]
97 | p
98 | `shouldBe` Document
99 | (Just (DocumentHeader [] 0 (MarkupLine "Foo" :| [])))
100 | [Paragraph [] (MarkupLine "Bar" :| [])],
101 | testCase "document title with level 1 followed by paragraph" $ do
102 | p <-
103 | parseDocument
104 | [ "== Foo",
105 | "Bar"
106 | ]
107 | p
108 | `shouldBe` Document
109 | (Just (DocumentHeader [] 1 (MarkupLine "Foo" :| [])))
110 | [Paragraph [] (MarkupLine "Bar" :| [])],
111 | testCase "document with no title and a paragraph" $ do
112 | p <-
113 | parseDocument
114 | [ "Foo"
115 | ]
116 | p
117 | `shouldBe` Document
118 | Nothing
119 | [Paragraph [] (MarkupLine "Foo" :| [])],
120 | testCase "single dangling block prefix" $ do
121 | p <- parseDocument ["[[Foo]]"]
122 | p
123 | `shouldBe` Document Nothing [DanglingBlockPrefix [MetadataItem (BlockId "Foo")]],
124 | testCase "dangling block prefix at eof and after empty line" $ do
125 | p <-
126 | parseDocument
127 | [ "",
128 | "[[Foo]]"
129 | ]
130 | p `shouldBe` Document Nothing [DanglingBlockPrefix [MetadataItem (BlockId "Foo")]]
131 | ]
132 |
133 | blockCornerCaseUnitTests :: TestTree
134 | blockCornerCaseUnitTests =
135 | testGroup
136 | "block corner case unit tests"
137 | [ testCase "empty block list" $ do
138 | p <-
139 | parseBlocks
140 | []
141 | p `shouldBe` [],
142 | testCase "single empty line" $ do
143 | p <-
144 | parseBlocks
145 | [ ""
146 | ]
147 | p `shouldBe` []
148 | ]
149 |
150 | paragraphUnitTests :: TestTree
151 | paragraphUnitTests =
152 | testGroup
153 | "paragraph unit tests"
154 | [ testCase "one line paragraph" $ do
155 | p <-
156 | parseBlocks
157 | [ "Foo"
158 | ]
159 | p `shouldBe` [Paragraph [] (MarkupLine "Foo" :| [])],
160 | testCase "two lines paragraph" $ do
161 | p <-
162 | parseBlocks
163 | [ "Foo",
164 | "Bar"
165 | ]
166 | p `shouldBe` [Paragraph [] (MarkupLine "Foo" :| [MarkupLine "Bar"])],
167 | testCase "paragraph followed by empty line" $ do
168 | p <-
169 | parseBlocks
170 | [ "Foo",
171 | ""
172 | ]
173 | p `shouldBe` [Paragraph [] (MarkupLine "Foo" :| [])],
174 | testCase "paragraph with indented following lines" $ do
175 | p <-
176 | parseBlocks
177 | [ "Foo",
178 | " Bar",
179 | " Baz"
180 | ]
181 | p `shouldBe` [Paragraph [] (MarkupLine "Foo" :| [MarkupLine " Bar", MarkupLine " Baz"])],
182 | testCase "two paragraphs" $ do
183 | p <-
184 | parseBlocks
185 | [ "Foo",
186 | "",
187 | "Bar"
188 | ]
189 | p
190 | `shouldBe` [ Paragraph [] (MarkupLine "Foo" :| []),
191 | Paragraph [] (MarkupLine "Bar" :| [])
192 | ],
193 | testCase "paragraph with block prefix" $ do
194 | p <-
195 | parseBlocks
196 | [ ".Foo",
197 | "// Comment",
198 | "[Foo#Bar%Baz]",
199 | "Foo"
200 | ]
201 | p
202 | `shouldBe` [ Paragraph
203 | [ MetadataItem (BlockTitle (MarkupLine "Foo" :| [])),
204 | Comment (LineCommentSequence (" Comment" :| [])),
205 | MetadataItem (BlockAttributeList "Foo#Bar%Baz")
206 | ]
207 | (MarkupLine "Foo" :| [])
208 | ],
209 | testCase "paragraph with block prefix containing empty lines" $ do
210 | p <-
211 | parseBlocks
212 | [ ".Foo",
213 | "",
214 | "[Foo#Bar%Baz]",
215 | "",
216 | "Foo"
217 | ]
218 | p
219 | `shouldBe` [ Paragraph
220 | [ MetadataItem (BlockTitle (MarkupLine "Foo" :| [])),
221 | MetadataItem (BlockAttributeList "Foo#Bar%Baz")
222 | ]
223 | (MarkupLine "Foo" :| [])
224 | ],
225 | testCase "paragraph followed by dangling block prefix" $ do
226 | p <-
227 | parseBlocks
228 | [ "Foo",
229 | "",
230 | ".Foo"
231 | ]
232 | p
233 | `shouldBe` [ Paragraph
234 | []
235 | (MarkupLine "Foo" :| []),
236 | DanglingBlockPrefix
237 | [ MetadataItem (BlockTitle (MarkupLine "Foo" :| []))
238 | ]
239 | ],
240 | testCase "paragraph with second line resembling block title" $ do
241 | p <-
242 | parseBlocks
243 | [ "Foo",
244 | ".Bar"
245 | ]
246 | p
247 | `shouldBe` [ Paragraph
248 | []
249 | (MarkupLine "Foo" :| [MarkupLine ".Bar"])
250 | ]
251 | ]
252 |
253 | sectionHeaderUnitTests :: TestTree
254 | sectionHeaderUnitTests =
255 | testGroup
256 | "section header unit tests"
257 | [ testCase "level 0 section header" $ do
258 | p <-
259 | parseBlocks
260 | [ "= Foo"
261 | ]
262 | p
263 | `shouldBe` [ SectionHeader
264 | []
265 | 0
266 | (MarkupLine "Foo" :| [])
267 | ],
268 | testCase "level 1 section header" $ do
269 | p <-
270 | parseBlocks
271 | [ "== Foo"
272 | ]
273 | p
274 | `shouldBe` [ SectionHeader
275 | []
276 | 1
277 | (MarkupLine "Foo" :| [])
278 | ],
279 | testCase "level 2 section header" $ do
280 | p <-
281 | parseBlocks
282 | [ "=== Foo"
283 | ]
284 | p
285 | `shouldBe` [ SectionHeader
286 | []
287 | 2
288 | (MarkupLine "Foo" :| [])
289 | ],
290 | testCase "section header with two words" $ do
291 | p <-
292 | parseBlocks
293 | [ "= Foo bar"
294 | ]
295 | p
296 | `shouldBe` [ SectionHeader
297 | []
298 | 0
299 | (MarkupLine "Foo bar" :| [])
300 | ],
301 | testCase "section header beginning with space" $ do
302 | p <-
303 | parseBlocks
304 | [ "= Foo"
305 | ]
306 | p
307 | `shouldBe` [ SectionHeader
308 | []
309 | 0
310 | (MarkupLine "Foo" :| [])
311 | ],
312 | testCase "section header followed by paragraph" $ do
313 | p <-
314 | parseBlocks
315 | [ "= Foo",
316 | "Bar"
317 | ]
318 | p
319 | `shouldBe` [ SectionHeader
320 | []
321 | 0
322 | (MarkupLine "Foo" :| []),
323 | Paragraph [] (MarkupLine "Bar" :| [])
324 | ],
325 | testCase "section header followed by empty line and paragraph" $ do
326 | p <-
327 | parseBlocks
328 | [ "= Foo",
329 | "",
330 | "Bar"
331 | ]
332 | p
333 | `shouldBe` [ SectionHeader
334 | []
335 | 0
336 | (MarkupLine "Foo" :| []),
337 | Paragraph [] (MarkupLine "Bar" :| [])
338 | ],
339 | testCase "section header with block prefix" $ do
340 | p <-
341 | parseBlocks
342 | [ ".Foo",
343 | "= Foo"
344 | ]
345 | p
346 | `shouldBe` [ SectionHeader
347 | [MetadataItem (BlockTitle (MarkupLine "Foo" :| []))]
348 | 0
349 | (MarkupLine "Foo" :| [])
350 | ],
351 | testCase "section header followed by paragraph with block prefix" $ do
352 | p <-
353 | parseBlocks
354 | [ "= Foo",
355 | ".Bar",
356 | "Bar"
357 | ]
358 | p
359 | `shouldBe` [ SectionHeader
360 | []
361 | 0
362 | (MarkupLine "Foo" :| []),
363 | Paragraph
364 | [MetadataItem (BlockTitle (MarkupLine "Bar" :| []))]
365 | (MarkupLine "Bar" :| [])
366 | ],
367 | testCase "discrete section header" $ do
368 | p <-
369 | parseBlocks
370 | [ "[discrete]",
371 | "= Foo"
372 | ]
373 | p
374 | `shouldBe` [ SectionHeader
375 | [MetadataItem (BlockAttributeList "discrete")]
376 | 0
377 | (MarkupLine "Foo" :| [])
378 | ]
379 | case p of
380 | (SectionHeader prefix _ _) : _ ->
381 | toMetadata prefix
382 | `shouldBe` (mempty @(Metadata UnparsedInline))
383 | { metadataStyle =
384 | Just
385 | ( Last
386 | { getLast = "discrete"
387 | }
388 | )
389 | }
390 | _ -> error "test case: discrete section header",
391 | -- TODO. Must change when indented literal paragraphs are implemented.
392 | testCase "false section header (space before '=')" $ do
393 | p <-
394 | parseBlocks
395 | [ " = Foo"
396 | ]
397 | p `shouldBe` [Paragraph [] (MarkupLine " = Foo" :| [])]
398 | ]
399 |
400 | danglingBlockPrefixUnitTests :: TestTree
401 | danglingBlockPrefixUnitTests =
402 | testGroup
403 | "dangling block prefix unit tests"
404 | [ testCase "dangling block prefix at eof and after paragraph" $ do
405 | p <-
406 | parseBlocks
407 | [ "Foo",
408 | "[[Foo]]"
409 | ]
410 | p
411 | `shouldBe` [ Paragraph
412 | []
413 | (MarkupLine "Foo" :| []),
414 | DanglingBlockPrefix [MetadataItem (BlockId "Foo")]
415 | ],
416 | testCase "dangling block prefix at end of example block" $ do
417 | p <-
418 | parseBlocks
419 | [ "====",
420 | "Foo",
421 | "",
422 | "[[Bar]]",
423 | "===="
424 | ]
425 | p
426 | `shouldBe` [ Nestable
427 | Example
428 | []
429 | [ Paragraph [] (MarkupLine "Foo" :| []),
430 | DanglingBlockPrefix [MetadataItem (BlockId "Bar")]
431 | ]
432 | ]
433 | ]
434 |
435 | nestableUnitTests :: TestTree
436 | nestableUnitTests =
437 | testGroup
438 | "nestable block unit tests"
439 | [ testCase "simple example block" $ do
440 | p <-
441 | parseBlocks
442 | [ "====",
443 | "Foo",
444 | "===="
445 | ]
446 | p
447 | `shouldBe` [ Nestable
448 | Example
449 | []
450 | [Paragraph [] (MarkupLine "Foo" :| [])]
451 | ],
452 | testCase "simple sidebar block" $ do
453 | p <-
454 | parseBlocks
455 | [ "****",
456 | "Foo",
457 | "****"
458 | ]
459 | p
460 | `shouldBe` [ Nestable
461 | Sidebar
462 | []
463 | [Paragraph [] (MarkupLine "Foo" :| [])]
464 | ],
465 | testCase "example block containing two paragraphs" $ do
466 | p <-
467 | parseBlocks
468 | [ "====",
469 | "Foo",
470 | "",
471 | "Bar",
472 | "===="
473 | ]
474 | p
475 | `shouldBe` [ Nestable
476 | Example
477 | []
478 | [ Paragraph [] (MarkupLine "Foo" :| []),
479 | Paragraph [] (MarkupLine "Bar" :| [])
480 | ]
481 | ],
482 | testCase "example block with block title" $ do
483 | p <-
484 | parseBlocks
485 | [ ".Foo",
486 | "====",
487 | "Bar",
488 | "===="
489 | ]
490 | p
491 | `shouldBe` [ Nestable
492 | Example
493 | [MetadataItem (BlockTitle (MarkupLine "Foo" :| []))]
494 | [Paragraph [] (MarkupLine "Bar" :| [])]
495 | ],
496 | testCase "sidebar nested into example block" $ do
497 | p <-
498 | parseBlocks
499 | [ "====",
500 | "****",
501 | "Bar",
502 | "****",
503 | "===="
504 | ]
505 | p
506 | `shouldBe` [ Nestable
507 | Example
508 | []
509 | [ Nestable
510 | Sidebar
511 | []
512 | [ Paragraph [] (MarkupLine "Bar" :| [])
513 | ]
514 | ]
515 | ],
516 | testCase "sidebar nested into example block and following paragraph" $ do
517 | p <-
518 | parseBlocks
519 | [ "====",
520 | "Foo",
521 | "****",
522 | "Bar",
523 | "****",
524 | "===="
525 | ]
526 | p
527 | `shouldBe` [ Nestable
528 | Example
529 | []
530 | [ Paragraph [] (MarkupLine "Foo" :| []),
531 | Nestable
532 | Sidebar
533 | []
534 | [ Paragraph [] (MarkupLine "Bar" :| [])
535 | ]
536 | ]
537 | ],
538 | testCase "sidebar nested into example block and following empty line" $ do
539 | p <-
540 | parseBlocks
541 | [ "====",
542 | "",
543 | "****",
544 | "Bar",
545 | "****",
546 | "===="
547 | ]
548 | p
549 | `shouldBe` [ Nestable
550 | Example
551 | []
552 | [ Nestable
553 | Sidebar
554 | []
555 | [ Paragraph [] (MarkupLine "Bar" :| [])
556 | ]
557 | ]
558 | ],
559 | testCase "example block nested into example block and following paragraph" $ do
560 | p <-
561 | parseBlocks
562 | [ "====",
563 | "Foo",
564 | "======",
565 | "Bar",
566 | "======",
567 | "===="
568 | ]
569 | p
570 | `shouldBe` [ Nestable
571 | Example
572 | []
573 | [ Paragraph [] (MarkupLine "Foo" :| []),
574 | Nestable
575 | Example
576 | []
577 | [ Paragraph [] (MarkupLine "Bar" :| [])
578 | ]
579 | ]
580 | ],
581 | testCase "non-closed sidebar nested into example block" $ do
582 | p <-
583 | parseBlocks
584 | [ "====",
585 | "Foo",
586 | "****",
587 | "Bar",
588 | "===="
589 | ]
590 | p
591 | `shouldBe` [ Nestable
592 | Example
593 | []
594 | [ Paragraph [] (MarkupLine "Foo" :| []),
595 | Nestable
596 | Sidebar
597 | []
598 | [ Paragraph [] (MarkupLine "Bar" :| [])
599 | ]
600 | ]
601 | ],
602 | testCase "non-closed example block nested into example block" $ do
603 | p <-
604 | parseBlocks
605 | [ "====",
606 | "Foo",
607 | "======",
608 | "Bar",
609 | "===="
610 | ]
611 | p
612 | `shouldBe` [ Nestable
613 | Example
614 | []
615 | [ Paragraph [] (MarkupLine "Foo" :| []),
616 | Nestable
617 | Example
618 | []
619 | [ Paragraph [] (MarkupLine "Bar" :| [])
620 | ]
621 | ]
622 | ]
623 | ]
624 |
625 | unorderedListUnitTests :: TestTree
626 | unorderedListUnitTests =
627 | testGroup
628 | "unordered list unit tests"
629 | [ testCase "simple unordered list using '-' (hyphen)" $ do
630 | p <-
631 | parseBlocks
632 | [ "- Foo",
633 | "- Bar",
634 | "- Baz"
635 | ]
636 | p
637 | `shouldBe` [ List
638 | (Unordered Nothing)
639 | []
640 | ( (Paragraph [] (MarkupLine "Foo" :| []) :| [])
641 | :| [ Paragraph [] (MarkupLine "Bar" :| []) :| [],
642 | Paragraph [] (MarkupLine "Baz" :| []) :| []
643 | ]
644 | )
645 | ],
646 | testCase "simple unordered list using '*' (asterisk)" $ do
647 | p <-
648 | parseBlocks
649 | [ "* Foo",
650 | "* Bar",
651 | "* Baz"
652 | ]
653 | p
654 | `shouldBe` [ List
655 | (Unordered Nothing)
656 | []
657 | ( (Paragraph [] (MarkupLine "Foo" :| []) :| [])
658 | :| [ Paragraph [] (MarkupLine "Bar" :| []) :| [],
659 | Paragraph [] (MarkupLine "Baz" :| []) :| []
660 | ]
661 | )
662 | ],
663 | testCase "unordered list with irregular indentation" $ do
664 | p <-
665 | parseBlocks
666 | [ "* Foo",
667 | " * Bar",
668 | " * Baz"
669 | ]
670 | p
671 | `shouldBe` [ List
672 | (Unordered Nothing)
673 | []
674 | ( (Paragraph [] (MarkupLine "Foo" :| []) :| [])
675 | :| [ Paragraph [] (MarkupLine "Bar" :| []) :| [],
676 | Paragraph [] (MarkupLine "Baz" :| []) :| []
677 | ]
678 | )
679 | ],
680 | testCase "unordered list with indented first item" $ do
681 | p <-
682 | parseBlocks
683 | [ "Foo",
684 | "",
685 | " * Bar",
686 | "* Baz"
687 | ]
688 | p
689 | `shouldBe` [ Paragraph [] (MarkupLine "Foo" :| []),
690 | List
691 | (Unordered Nothing)
692 | []
693 | ( (Paragraph [] (MarkupLine "Bar" :| []) :| [])
694 | :| [ Paragraph [] (MarkupLine "Baz" :| []) :| []
695 | ]
696 | )
697 | ],
698 | testCase "unordered list with irregular line spacing" $ do
699 | p <-
700 | parseBlocks
701 | [ "* Foo",
702 | "",
703 | "",
704 | "* Bar",
705 | "",
706 | "* Baz"
707 | ]
708 | p
709 | `shouldBe` [ List
710 | (Unordered Nothing)
711 | []
712 | ( (Paragraph [] (MarkupLine "Foo" :| []) :| [])
713 | :| [ Paragraph [] (MarkupLine "Bar" :| []) :| [],
714 | Paragraph [] (MarkupLine "Baz" :| []) :| []
715 | ]
716 | )
717 | ],
718 | testCase "unordered list with multi-line paragraphs" $ do
719 | p <-
720 | parseBlocks
721 | [ "* Foo",
722 | "Bar",
723 | "* Baz",
724 | " Qux"
725 | ]
726 | p
727 | `shouldBe` [ List
728 | (Unordered Nothing)
729 | []
730 | ( (Paragraph [] (MarkupLine "Foo" :| [MarkupLine "Bar"]) :| [])
731 | :| [Paragraph [] (MarkupLine "Baz" :| [MarkupLine " Qux"]) :| []]
732 | )
733 | ],
734 | testCase "unordered list item with literal second paragraph" $ do
735 | p <-
736 | parseBlocks
737 | [ "* Foo",
738 | "",
739 | " Bar"
740 | ]
741 | p
742 | `shouldBe` [ List
743 | (Unordered Nothing)
744 | []
745 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
746 | -- TODO. Add when indented literal paragraphs are
747 | -- implemented:
748 | --
749 | -- :| [QUOTED [] (MarkupLine "Bar" :| [])]
750 | :| []
751 | )
752 | :| []
753 | ),
754 | -- TODO. Remove when indented literal paragraphs are
755 | -- implemented:
756 | Paragraph [] (MarkupLine " Bar" :| [])
757 | ],
758 | testCase "two unordered lists separated by a paragraph" $ do
759 | p <-
760 | parseBlocks
761 | [ "* Foo",
762 | "",
763 | "Bar",
764 | "",
765 | "* Baz",
766 | "* Qux"
767 | ]
768 | p
769 | `shouldBe` [ List
770 | (Unordered Nothing)
771 | []
772 | ((Paragraph [] (MarkupLine "Foo" :| []) :| []) :| []),
773 | Paragraph [] (MarkupLine "Bar" :| []),
774 | List
775 | (Unordered Nothing)
776 | []
777 | ( (Paragraph [] (MarkupLine "Baz" :| []) :| [])
778 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
779 | )
780 | ],
781 | testCase "unordered list followed up by consecutive example block" $ do
782 | p <-
783 | parseBlocks
784 | [ "* Foo",
785 | "====",
786 | "Bar",
787 | "===="
788 | ]
789 | p
790 | `shouldBe` [ List
791 | (Unordered Nothing)
792 | []
793 | ((Paragraph [] (MarkupLine "Foo" :| []) :| []) :| []),
794 | Nestable
795 | Example
796 | []
797 | [Paragraph [] (MarkupLine "Bar" :| [])]
798 | ]
799 | ]
800 |
801 | nestedListsUnitTests :: TestTree
802 | nestedListsUnitTests =
803 | testGroup
804 | "nested lists unit tests"
805 | [ testCase "unordered list using '-' (hyphen) nested into list using '*' (asterisk)" $ do
806 | p <-
807 | parseBlocks
808 | [ "* Foo",
809 | "- Bar",
810 | "- Baz",
811 | "* Qux"
812 | ]
813 | p
814 | `shouldBe` [ List
815 | (Unordered Nothing)
816 | []
817 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
818 | :| [ List
819 | (Unordered Nothing)
820 | []
821 | ( (Paragraph [] (MarkupLine "Bar" :| []) :| [])
822 | :| [Paragraph [] (MarkupLine "Baz" :| []) :| []]
823 | )
824 | ]
825 | )
826 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
827 | )
828 | ],
829 | testCase "nested unordered lists using increasing number of '*' (asterisk)" $ do
830 | p <-
831 | parseBlocks
832 | [ "* Foo",
833 | "** Bar",
834 | "*** Baz",
835 | "* Qux"
836 | ]
837 | p
838 | `shouldBe` [ List
839 | (Unordered Nothing)
840 | []
841 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
842 | :| [ List
843 | (Unordered Nothing)
844 | []
845 | ( ( Paragraph [] (MarkupLine "Bar" :| [])
846 | :| [ List
847 | (Unordered Nothing)
848 | []
849 | ((Paragraph [] (MarkupLine "Baz" :| []) :| []) :| [])
850 | ]
851 | )
852 | :| []
853 | )
854 | ]
855 | )
856 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
857 | )
858 | ],
859 | testCase "nested unordered lists with empty lines interspersed" $ do
860 | p <-
861 | parseBlocks
862 | [ "* Foo",
863 | "",
864 | "** Bar",
865 | "",
866 | "*** Baz",
867 | "",
868 | "* Qux"
869 | ]
870 | p
871 | `shouldBe` [ List
872 | (Unordered Nothing)
873 | []
874 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
875 | :| [ List
876 | (Unordered Nothing)
877 | []
878 | ( ( Paragraph [] (MarkupLine "Bar" :| [])
879 | :| [ List
880 | (Unordered Nothing)
881 | []
882 | ((Paragraph [] (MarkupLine "Baz" :| []) :| []) :| [])
883 | ]
884 | )
885 | :| []
886 | )
887 | ]
888 | )
889 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
890 | )
891 | ],
892 | testCase "nested unordered lists using unordered number of '*' (asterisk)" $ do
893 | p <-
894 | parseBlocks
895 | [ "** Foo",
896 | "* Bar",
897 | "*** Baz",
898 | " ** Qux"
899 | ]
900 | p
901 | `shouldBe` [ List
902 | (Unordered Nothing)
903 | []
904 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
905 | :| [ List
906 | (Unordered Nothing)
907 | []
908 | ( ( Paragraph [] (MarkupLine "Bar" :| [])
909 | :| [ List
910 | (Unordered Nothing)
911 | []
912 | ((Paragraph [] (MarkupLine "Baz" :| []) :| []) :| [])
913 | ]
914 | )
915 | :| []
916 | )
917 | ]
918 | )
919 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
920 | )
921 | ],
922 | testCase "nested unordered list with multi-line paragraph" $ do
923 | p <-
924 | parseBlocks
925 | [ "* Foo",
926 | "- Bar",
927 | "Baz",
928 | "* Qux"
929 | ]
930 | p
931 | `shouldBe` [ List
932 | (Unordered Nothing)
933 | []
934 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
935 | :| [ List
936 | (Unordered Nothing)
937 | []
938 | ( (Paragraph [] (MarkupLine "Bar" :| [MarkupLine "Baz"]) :| [])
939 | :| []
940 | )
941 | ]
942 | )
943 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
944 | )
945 | ],
946 | testCase "(DVB001) nested unordered list with block prefixes" $ do
947 | p <-
948 | parseBlocks
949 | [ "[.red]",
950 | ".FooFoo",
951 | "* Foo",
952 | "[.blue]",
953 | ".BarBar",
954 | "- Bar",
955 | "[.green]",
956 | ".BazBaz",
957 | "- Baz"
958 | ]
959 | p
960 | `shouldBe` [ List
961 | (Unordered Nothing)
962 | [ MetadataItem (BlockAttributeList ".red"),
963 | MetadataItem (BlockTitle (MarkupLine "FooFoo" :| []))
964 | ]
965 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
966 | :| [ List
967 | (Unordered Nothing)
968 | [ MetadataItem (BlockAttributeList ".blue"),
969 | MetadataItem (BlockTitle (MarkupLine "BarBar" :| []))
970 | ]
971 | ( (Paragraph [] (MarkupLine "Bar" :| []) :| [])
972 | :| []
973 | )
974 | ]
975 | )
976 | :| []
977 | ),
978 | List
979 | (Unordered Nothing)
980 | [ MetadataItem (BlockAttributeList ".green"),
981 | MetadataItem (BlockTitle (MarkupLine "BazBaz" :| []))
982 | ]
983 | ((Paragraph [] (MarkupLine "Baz" :| []) :| []) :| [])
984 | ],
985 | -- Identical result to the previous test case.
986 | testCase "(DVB001) nested unordered list with block prefixes and some empty lines" $ do
987 | p <-
988 | parseBlocks
989 | [ "[.red]",
990 | ".FooFoo",
991 | "",
992 | "* Foo",
993 | "[.blue]",
994 | "",
995 | ".BarBar",
996 | "- Bar",
997 | "",
998 | "[.green]",
999 | ".BazBaz",
1000 | "- Baz"
1001 | ]
1002 | p
1003 | `shouldBe` [ List
1004 | (Unordered Nothing)
1005 | [ MetadataItem (BlockAttributeList ".red"),
1006 | MetadataItem (BlockTitle (MarkupLine "FooFoo" :| []))
1007 | ]
1008 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1009 | :| [ List
1010 | (Unordered Nothing)
1011 | [ MetadataItem (BlockAttributeList ".blue"),
1012 | MetadataItem (BlockTitle (MarkupLine "BarBar" :| []))
1013 | ]
1014 | ( (Paragraph [] (MarkupLine "Bar" :| []) :| [])
1015 | :| []
1016 | )
1017 | ]
1018 | )
1019 | :| []
1020 | ),
1021 | List
1022 | (Unordered Nothing)
1023 | [ MetadataItem (BlockAttributeList ".green"),
1024 | MetadataItem (BlockTitle (MarkupLine "BazBaz" :| []))
1025 | ]
1026 | ((Paragraph [] (MarkupLine "Baz" :| []) :| []) :| [])
1027 | ]
1028 | ]
1029 |
1030 | listContinuationUnitTests :: TestTree
1031 | listContinuationUnitTests =
1032 | testGroup
1033 | "list continuation unit tests"
1034 | [ testCase "list continuation (paragraph), followed by another list item" $ do
1035 | p <-
1036 | parseBlocks
1037 | [ "* Foo",
1038 | "+",
1039 | "Bar",
1040 | "* Baz"
1041 | ]
1042 | p
1043 | `shouldBe` [ List
1044 | (Unordered Nothing)
1045 | []
1046 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1047 | :| [Paragraph [] (MarkupLine "Bar" :| [])]
1048 | )
1049 | :| [Paragraph [] (MarkupLine "Baz" :| []) :| []]
1050 | )
1051 | ],
1052 | testCase "two list continuations (paragraph), followed by another list item" $ do
1053 | p <-
1054 | parseBlocks
1055 | [ "* Foo",
1056 | "+",
1057 | "Bar",
1058 | "+",
1059 | "Baz",
1060 | "* Qux"
1061 | ]
1062 | p
1063 | `shouldBe` [ List
1064 | (Unordered Nothing)
1065 | []
1066 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1067 | :| [ Paragraph [] (MarkupLine "Bar" :| []),
1068 | Paragraph [] (MarkupLine "Baz" :| [])
1069 | ]
1070 | )
1071 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
1072 | )
1073 | ],
1074 | testCase "list continuation (paragraph) with a block prefix" $ do
1075 | p <-
1076 | parseBlocks
1077 | [ "* Foo",
1078 | "+",
1079 | "[.red]",
1080 | "Bar"
1081 | ]
1082 | p
1083 | `shouldBe` [ List
1084 | (Unordered Nothing)
1085 | []
1086 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1087 | :| [ Paragraph
1088 | [MetadataItem (BlockAttributeList ".red")]
1089 | (MarkupLine "Bar" :| [])
1090 | ]
1091 | )
1092 | :| []
1093 | )
1094 | ],
1095 | testCase "list continuation (example block), followed by another list item" $ do
1096 | p <-
1097 | parseBlocks
1098 | [ "* Foo",
1099 | "+",
1100 | "====",
1101 | "Bar",
1102 | "====",
1103 | "* Baz"
1104 | ]
1105 | p
1106 | `shouldBe` [ List
1107 | (Unordered Nothing)
1108 | []
1109 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1110 | :| [Nestable Example [] [Paragraph [] (MarkupLine "Bar" :| [])]]
1111 | )
1112 | :| [Paragraph [] (MarkupLine "Baz" :| []) :| []]
1113 | )
1114 | ],
1115 | testCase "list continuation into a nested unordered list" $ do
1116 | p <-
1117 | parseBlocks
1118 | [ "* Foo",
1119 | "** Bar",
1120 | "+",
1121 | "Baz",
1122 | "* Qux"
1123 | ]
1124 | p
1125 | `shouldBe` [ List
1126 | (Unordered Nothing)
1127 | []
1128 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1129 | :| [ List
1130 | (Unordered Nothing)
1131 | []
1132 | ( ( Paragraph [] (MarkupLine "Bar" :| [])
1133 | :| [Paragraph [] (MarkupLine "Baz" :| [])]
1134 | )
1135 | :| []
1136 | )
1137 | ]
1138 | )
1139 | :| [Paragraph [] (MarkupLine "Qux" :| []) :| []]
1140 | )
1141 | ],
1142 | testCase "dangling list continuation marker in outermost list" $ do
1143 | p <-
1144 | parseBlocks
1145 | [ "* Foo",
1146 | "+",
1147 | "", -- Asciidoctor allows this optional empty line here
1148 | "",
1149 | "Bar"
1150 | ]
1151 | p
1152 | `shouldBe` [ List
1153 | (Unordered Nothing)
1154 | []
1155 | ((Paragraph [] (MarkupLine "Foo" :| []) :| []) :| []),
1156 | Paragraph [] (MarkupLine "Bar" :| [])
1157 | ],
1158 | testCase "dangling list continuation marker into a nested unordered list" $ do
1159 | p <-
1160 | parseBlocks
1161 | [ "* Foo",
1162 | "** Bar",
1163 | "+",
1164 | "", -- Asciidoctor allows this optional blank line here
1165 | "",
1166 | "Baz"
1167 | ]
1168 | p
1169 | `shouldBe` [ List
1170 | (Unordered Nothing)
1171 | []
1172 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1173 | :| [ List
1174 | (Unordered Nothing)
1175 | []
1176 | ((Paragraph [] (MarkupLine "Bar" :| []) :| []) :| [])
1177 | ]
1178 | )
1179 | :| []
1180 | ),
1181 | Paragraph [] (MarkupLine "Baz" :| [])
1182 | ],
1183 | testCase "(DVB002) broken list continuation attempt in outermost list" $ do
1184 | p <-
1185 | parseBlocks
1186 | [ "* Foo",
1187 | "",
1188 | "+",
1189 | "Bar"
1190 | ]
1191 | p
1192 | `shouldBe` [ List
1193 | (Unordered Nothing)
1194 | []
1195 | ((Paragraph [] (MarkupLine "Foo" :| []) :| []) :| []),
1196 | Paragraph [] (MarkupLine "+" :| [MarkupLine "Bar"])
1197 | ],
1198 | testCase "(DVB002) broken list continuation attempt in nested list" $ do
1199 | p <-
1200 | parseBlocks
1201 | [ "* Foo",
1202 | "** Bar",
1203 | "",
1204 | "+",
1205 | "Baz"
1206 | ]
1207 | p
1208 | `shouldBe` [ List
1209 | (Unordered Nothing)
1210 | []
1211 | ( ( Paragraph [] (MarkupLine "Foo" :| [])
1212 | :| [ List
1213 | (Unordered Nothing)
1214 | []
1215 | ((Paragraph [] (MarkupLine "Bar" :| []) :| []) :| [])
1216 | ]
1217 | )
1218 | :| []
1219 | ),
1220 | Paragraph [] (MarkupLine "+" :| [MarkupLine "Baz"])
1221 | ],
1222 | testCase "line break that resembles list continuation" $ do
1223 | p <-
1224 | parseBlocks
1225 | [ "* Foo",
1226 | " +",
1227 | "Bar"
1228 | ]
1229 | p
1230 | `shouldBe` [ List
1231 | (Unordered Nothing)
1232 | []
1233 | -- TODO. Must be changed when line breaks are
1234 | -- implemented.
1235 | ( ( Paragraph
1236 | []
1237 | ( MarkupLine "Foo"
1238 | :| [ MarkupLine " +",
1239 | MarkupLine "Bar"
1240 | ]
1241 | )
1242 | :| []
1243 | )
1244 | :| []
1245 | )
1246 | ]
1247 | ]
1248 |
1249 | commentUnitTests :: TestTree
1250 | commentUnitTests =
1251 | testGroup
1252 | "comment unit tests"
1253 | [ testCase "dangling block comment" $ do
1254 | p <-
1255 | parseBlocks
1256 | [ "////",
1257 | "Foo",
1258 | "////"
1259 | ]
1260 | p
1261 | `shouldBe` [DanglingBlockPrefix [Comment (BlockComment ["Foo"])]],
1262 | testCase "dangling line comment sequence" $ do
1263 | p <-
1264 | parseBlocks
1265 | [ "//Foo",
1266 | "// Bar"
1267 | ]
1268 | p
1269 | `shouldBe` [ DanglingBlockPrefix
1270 | [Comment (LineCommentSequence ("Foo" :| [" Bar"]))]
1271 | ],
1272 | testCase "block comment before paragraph" $ do
1273 | p <-
1274 | parseBlocks
1275 | [ "////",
1276 | "Foo",
1277 | "////",
1278 | "Bar"
1279 | ]
1280 | p
1281 | `shouldBe` [ Paragraph
1282 | [Comment (BlockComment ["Foo"])]
1283 | (MarkupLine "Bar" :| [])
1284 | ],
1285 | testCase "block comment before paragraph, with redundant space" $ do
1286 | p <-
1287 | parseBlocks
1288 | [ "//// ",
1289 | "Foo",
1290 | "//// ",
1291 | "Bar"
1292 | ]
1293 | p
1294 | `shouldBe` [ Paragraph
1295 | [Comment (BlockComment ["Foo"])]
1296 | (MarkupLine "Bar" :| [])
1297 | ],
1298 | testCase "block comment before paragraph, separated by blank line" $ do
1299 | p <-
1300 | parseBlocks
1301 | [ "////",
1302 | "Foo",
1303 | "////",
1304 | "",
1305 | "Bar"
1306 | ]
1307 | p
1308 | `shouldBe` [ Paragraph
1309 | [Comment (BlockComment ["Foo"])]
1310 | (MarkupLine "Bar" :| [])
1311 | ],
1312 | testCase "empty block comment before paragraph" $ do
1313 | p <-
1314 | parseBlocks
1315 | [ "////",
1316 | "////",
1317 | "Foo"
1318 | ]
1319 | p
1320 | `shouldBe` [ Paragraph
1321 | [Comment (BlockComment [])]
1322 | (MarkupLine "Foo" :| [])
1323 | ],
1324 | testCase "empty line comment before paragraph" $ do
1325 | p <-
1326 | parseBlocks
1327 | [ "//",
1328 | "Foo"
1329 | ]
1330 | p
1331 | `shouldBe` [ Paragraph
1332 | [Comment (LineCommentSequence ("" :| []))]
1333 | (MarkupLine "Foo" :| [])
1334 | ],
1335 | testCase "block comment with multiple pseudo-paragraphs" $ do
1336 | p <-
1337 | parseBlocks
1338 | [ "////",
1339 | "Foo",
1340 | "",
1341 | "Bar",
1342 | "////",
1343 | "Baz"
1344 | ]
1345 | p
1346 | `shouldBe` [ Paragraph
1347 | [Comment (BlockComment ["Foo", "", "Bar"])]
1348 | (MarkupLine "Baz" :| [])
1349 | ],
1350 | testCase "line comment sequence before paragraph" $ do
1351 | p <-
1352 | parseBlocks
1353 | [ "//Foo",
1354 | "// Bar",
1355 | "Baz"
1356 | ]
1357 | p
1358 | `shouldBe` [ Paragraph
1359 | [Comment (LineCommentSequence ("Foo" :| [" Bar"]))]
1360 | (MarkupLine "Baz" :| [])
1361 | ],
1362 | testCase "line comment inside paragraph" $ do
1363 | p <-
1364 | parseBlocks
1365 | [ "Foo",
1366 | "// Bar",
1367 | "Baz"
1368 | ]
1369 | p
1370 | `shouldBe` [ Paragraph
1371 | []
1372 | (MarkupLine "Foo" :| [CommentLine " Bar", MarkupLine "Baz"])
1373 | ],
1374 | testCase "line comment inside paragraph and after paragraph" $ do
1375 | p <-
1376 | parseBlocks
1377 | [ "Foo",
1378 | "// Bar",
1379 | "Baz",
1380 | "//Qux"
1381 | ]
1382 | p
1383 | `shouldBe` [ Paragraph
1384 | []
1385 | ( MarkupLine "Foo"
1386 | :| [ CommentLine " Bar",
1387 | MarkupLine "Baz",
1388 | CommentLine "Qux"
1389 | ]
1390 | )
1391 | ],
1392 | testCase "line comment sequence before section header" $ do
1393 | p <-
1394 | parseBlocks
1395 | [ "//Foo",
1396 | "// Bar",
1397 | "== Baz"
1398 | ]
1399 | p
1400 | `shouldBe` [ SectionHeader
1401 | [Comment (LineCommentSequence ("Foo" :| [" Bar"]))]
1402 | 1
1403 | (MarkupLine "Baz" :| [])
1404 | ],
1405 | testCase "block comment followed by line comment sequence" $ do
1406 | p <-
1407 | parseBlocks
1408 | [ "////",
1409 | "Foo",
1410 | "",
1411 | "////",
1412 | "//Bar",
1413 | "Baz"
1414 | ]
1415 | p
1416 | `shouldBe` [ Paragraph
1417 | [ Comment (BlockComment ["Foo", ""]),
1418 | Comment (LineCommentSequence ("Bar" :| []))
1419 | ]
1420 | (MarkupLine "Baz" :| [])
1421 | ],
1422 | testCase "line comment sequence followed by block comment" $ do
1423 | p <-
1424 | parseBlocks
1425 | [ "// Foo",
1426 | "//Bar",
1427 | "////",
1428 | "Baz",
1429 | "////",
1430 | "Qux"
1431 | ]
1432 | p
1433 | `shouldBe` [ Paragraph
1434 | [ Comment (LineCommentSequence (" Foo" :| ["Bar"])),
1435 | Comment (BlockComment ["Baz"])
1436 | ]
1437 | (MarkupLine "Qux" :| [])
1438 | ],
1439 | testCase "block comment with more than four '/' (slash)" $ do
1440 | p <-
1441 | parseBlocks
1442 | [ "/////",
1443 | "Foo",
1444 | "////",
1445 | "Bar",
1446 | "////",
1447 | "/////",
1448 | "Baz"
1449 | ]
1450 | p
1451 | `shouldBe` [ Paragraph
1452 | [ Comment (BlockComment ["Foo", "////", "Bar", "////"])
1453 | ]
1454 | (MarkupLine "Baz" :| [])
1455 | ],
1456 | testCase "dangling non-closed block comment" $ do
1457 | p <-
1458 | parseBlocks
1459 | [ "////",
1460 | "Foo",
1461 | "",
1462 | "Bar"
1463 | ]
1464 | p
1465 | `shouldBe` [ DanglingBlockPrefix
1466 | [Comment (BlockComment ["Foo", "", "Bar"])]
1467 | ],
1468 | testCase "bad block comment opening, with three '/' (slashes)" $ do
1469 | p <-
1470 | parseBlocks
1471 | [ "///",
1472 | "Foo",
1473 | "////"
1474 | ]
1475 | p
1476 | `shouldBe` [ Paragraph [] (MarkupLine "///" :| [MarkupLine "Foo"]),
1477 | DanglingBlockPrefix [Comment (BlockComment [])]
1478 | ],
1479 | testCase "bad line comment, with three '/' (slashes)" $ do
1480 | p <-
1481 | parseBlocks
1482 | [ "///Foo",
1483 | "Bar"
1484 | ]
1485 | p
1486 | `shouldBe` [Paragraph [] (MarkupLine "///Foo" :| [MarkupLine "Bar"])],
1487 | testCase "bad block comment opening, preceded by space" $ do
1488 | p <-
1489 | parseBlocks
1490 | [ " ////",
1491 | "Foo",
1492 | "////"
1493 | ]
1494 | p
1495 | `shouldBe` [ Paragraph [] (MarkupLine " ////" :| [MarkupLine "Foo"]),
1496 | DanglingBlockPrefix [Comment (BlockComment [])]
1497 | ],
1498 | testCase "bad line comment opening, preceded by space" $ do
1499 | p <-
1500 | parseBlocks
1501 | [ " //Foo",
1502 | "Bar"
1503 | ]
1504 | p
1505 | `shouldBe` [Paragraph [] (MarkupLine " //Foo" :| [MarkupLine "Bar"])]
1506 | ]
1507 |
--------------------------------------------------------------------------------