mmark (empty) → 0.0.1.0
raw patch · 23 files changed
+3640/−0 lines, 23 filesdep +QuickCheckdep +aesondep +basesetup-changed
Dependencies added: QuickCheck, aeson, base, containers, criterion, data-default-class, deepseq, email-validate, foldl, hspec, hspec-megaparsec, lucid, megaparsec, mmark, modern-uri, mtl, parser-combinators, semigroups, text, void, weigh, yaml
Files
- CHANGELOG.md +3/−0
- LICENSE.md +28/−0
- README.md +229/−0
- Setup.hs +6/−0
- Text/MMark.hs +114/−0
- Text/MMark/Extension.hs +113/−0
- Text/MMark/Internal.hs +419/−0
- Text/MMark/Parser.hs +762/−0
- bench/memory/Main.hs +27/−0
- bench/speed/Main.hs +27/−0
- data/bench-fenced-code-block.md +12/−0
- data/bench-heading.md +6/−0
- data/bench-indented-code-block.md +10/−0
- data/bench-paragraph.md +10/−0
- data/bench-thematic-break.md +6/−0
- data/bench-yaml-block.md +7/−0
- data/comprehensive.html +139/−0
- data/comprehensive.md +164/−0
- mmark.cabal +112/−0
- tests/Spec.hs +1/−0
- tests/Text/MMark/ExtensionSpec.hs +137/−0
- tests/Text/MMark/TestUtils.hs +111/−0
- tests/Text/MMarkSpec.hs +1197/−0
+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## MMark 0.0.1.0++* Initial release.
+ LICENSE.md view
@@ -0,0 +1,28 @@+Copyright © 2017 Mark Karpov++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice,+ this list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++* Neither the name Mark Karpov nor the names of contributors may be used to+ endorse or promote products derived from this software without specific+ prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN+NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,229 @@+# MMark++[](http://opensource.org/licenses/BSD-3-Clause)+[](https://hackage.haskell.org/package/mmark)+[](http://stackage.org/nightly/package/mmark)+[](http://stackage.org/lts/package/mmark)+[](https://travis-ci.org/mrkkrp/mmark)+[](https://coveralls.io/github/mrkkrp/mmark?branch=master)++MMark (read “em-mark”) is a strict markdown processor for writers. “Strict”+means that not every input is considered a valid markdown document and parse+errors are possible and even desirable, because they allow to spot markup+issues without searching for them in rendered document. If a markdown+document passes MMark parser, then it'll most certainly produce HTML without+quirks. This feature makes it a good choice for writers and bloggers.++MMark in its current state features:++* A parser that produces high-quality error messages and does not choke on+ first parse error. It is capable of reporting many parse errors where+ makes sense.+* An extension system allowing to create extensions that alter parsed+ markdown document in some way. Some of them are available in the+ [`mmark-ext`](https://hackage.haskell.org/package/mmark-ext) package.+* A [`lucid`](https://hackage.haskell.org/package/lucid)-based render.++There is also a blog post announcing the project:++https://markkarpov.com/post/announcing-mmark.html++## MMark and Common Mark++MMark mostly tries to follow the Common Mark specification as given here:++https://github.com/jgm/CommonMark++However, due to the fact that we do not allow inputs that do not make sense,+and also try to guard against common silly mistakes (like writing `##My+header` and having it rendered as a paragraph starting with hashes) MMark+obviously can't follow the specification precisely. In particular, parsing+of inlines differs considerably from Common Mark (see below).++Another difference between Common Mark and MMark is that the latter supports+more (pun alert) common markdown extensions out-of-the-box. In particular,+MMark supports:++* parsing of an optional YAML block+* strikeout using `~~this~~` syntax+* superscript using `^this^` syntax+* subscript using `~this~` syntax+* automatic assignment of ids to headers+* PHP-style footnotes, e.g. `[^1]` (NOT YET)+* “pipe” tables (as used on GitHub) (NOT YET)++One do not need to enable or tweak anything for these to work, they are+built-in features.++### Differences in inline parsing++Emphasis and strong emphasis is an especially hairy topic in the Common Mark+specification. There are 17 ad-hoc rules defining interaction between `*`+and `_` -based emphasis and more than half of all Common Mark examples+(that's about 300) test just this tricky logic.++Not only it is hard to implement (for tools built around markdown too), it's+hard to understand for humans too. For example, this input:++```+*(*foo*)*+```++produces this HTML:++```+<p><em>(<em>foo</em>)</em></p>+```++(Note the nested emphasis.)++Could it produce something like this instead?++```+<p><em>(</em>foo<em>)</em></p>+```++Well, why not? Without remembering those 17 ad-hoc rules, there going to be+a lot of tricky cases when a user won't be able to tell how markdown will be+parsed.++I decided to make parsing of emphasis, strong emphasis, and similar+constructs like strikethrough, subscript, and superscript more symmetric and+less ad-hoc. This is a work in progress and I'm not fully satisfied with the+current approach as it does not allow to express some combinations of+characters and markup, but in 99% of practical cases it is identical to+Common Mark, and normal markdown intuitions will work OK for the users.++Let's start by dividing all characters into three groups:++* **Markup characters**, including the following: `*`, `~`, `_`, `` ` ``,+ `^`, `[`, `]`. These are used for markup and whenever they appear in a+ document, they must form valid markup constructions. To be used as+ ordinary punctuation characters they must be backslash escaped.++* **Space characters**, including space, tab, newline, carriage return, and+ some Unicode space characters.++* **Other characters**, which include all characters not falling into the+ two groups described above.++**Markup characters** can be “converted” to **other characters** via+backslash escaping. We'll see how this is useful in a few moments.++We'll call **markdown characters** placed between **space characters** and+**other characters** *left-flanking delimiter run*. These markup characters+sort of hang on the left hand side of a word.++Similarly we'll call **markdown characters** placed between **other+characters** and **space characters** *right-flanking delimiter run*. These+hang on the right hand side of a word.++Emphasis markup (and other similar things like strikethrough, which we won't+mention explicitly anymore for brevity) can start only as left-flanking+delimiter run and end only as right-flanking delimiter run.++This produces a parse error:++```+*Something * is not right.+Something __is __ not right.+```++And this too:++```+__foo__bar+```++This means that inter-word emphasis is not supported by this approach. (This+is a pity, maybe I should adjust something to allow it.)++There is one more tricky thing. In some cases we want to end emphasis and+have full stop or other punctuation right after it:++```+Here it *goes*.+```++You can see that the closing `*` is not in right-flanking position here, and+so it's a parse error. To avoid this, some punctuation characters that+normally appear outside of markup were made “transparent” and thus they are+regarded as white space, so the example above parses correctly and works as+expected. To put a transparent character inside emphasis, backslash escaping+is necessary:++```+We *\(can\)* have it.+```++Here `(` and `)` are transparent punctuation characters, just like `.`, so+they must be turned into **other characters** to go inside the emphasis.+This is a corner case and should not be common in practice.++So far the main limitation of this approach is the pains with inter-word+markup, as in this example:++```+**We started to work on the *issue*.**+```++Should we escape `.` here? On one hand we should, to close `**`. But if we+do, the closing `*` won't be in right-flanking position anymore. God dammit.++### Other differences++Block-level parsing:++* Headings, thematic breaks, code blocks should be separated from paragraphs+ by at least one empty line. This makes the parser a lot simpler and forces+ markdown sources to be in a more readable form too.+* If a line starts with hash signs it is expected to be a valid *non-empty*+ header (level 1–6 inclusive). If you want to start a paragraph with+ hashes, just escape the first hash with backslash and that will be enough.+* Fenced code blocks must be explicitly closed by a closing fence. They are+ not closed by the end of document or by start of another block.++Inline-level parsing:++* MMark does not support hard line breaks represented as double space before+ newline. Nevertheless, hard line breaks in the form of backslash before+ newline are supported (these are more explicit too).+* All URI references (in links, images, autolinks, etc.) are parsed as per+ RFC 3986, no special escaping is supported. In addition to that, when a+ URI reference in not enclosed with `<` and `>`, then closing parenthesis+ character `)` is not considered part of URI (use `<uri>` syntax if you+ want closing parenthesis as part of a URI).+* Putting links in text of another link is not allowed, i.e. no nested links+ is possible.+* Putting images in description of other images is not allowed (similarly to+ the situation with links).++Not-yet-implemented things:++* Separate declaration of image's source and title is not (yet?) supported.+* Blockquotes are not implemented yet.+* Lists (unordered and ordered) are not implemented yet.+* Setext headings are not implemented yet.+* Reference links are not implemented yet.+* HTML blocks are not implemented yet.+* HTML inlines are not implemented yet.+* Entity and numeric character references are not implemented yet.++### Additional information about MMark-specific extensions++* YAML block must start with three hyphens `---` and end with three hyphens+ `---`. It can only be placed at the beginning of a markdown document.+ Trailing white space after the `---** sequences is tolerated.++## Contribution++Issues, bugs, and questions may be reported in [the GitHub issue tracker for+this project](https://github.com/mrkkrp/mmark/issues).++Pull requests are also welcome and will be reviewed quickly.++## License++Copyright © 2017 Mark Karpov++Distributed under BSD 3 clause license.
+ Setup.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Distribution.Simple++main :: IO ()+main = defaultMain
+ Text/MMark.hs view
@@ -0,0 +1,114 @@+-- |+-- Module : Text.MMark+-- Copyright : © 2017 Mark Karpov+-- License : BSD 3 clause+--+-- Maintainer : Mark Karpov <markkarpov92@gmail.com>+-- Stability : experimental+-- Portability : portable+--+-- MMark (read “em-mark”) is a strict markdown processor for writers.+-- “Strict” means that not every input is considered a valid markdown+-- document and parse errors are possible and even desirable, because they+-- allow to spot markup issues without searching for them in rendered+-- document. If a markdown document passes MMark parser, then it'll most+-- certainly produce HTML without quirks. This feature makes it a good+-- choice for writers and bloggers.+--+-- === MMark and Common Mark+--+-- MMark mostly tries to follow the Common Mark specification as given here:+--+-- <https://github.com/jgm/CommonMark>+--+-- However, due to the fact that we do not allow inputs that do not make+-- sense, and also try to guard against common silly mistakes (like writing+-- @##My header@ and having it rendered as a paragraph starting with hashes)+-- MMark obviously can't follow the specification precisely. In particular,+-- parsing of inlines differs considerably from Common Mark.+--+-- Another difference between Common Mark and MMark is that the latter+-- supports more (pun alert) common markdown extensions out-of-the-box. In+-- particular, MMark supports:+--+-- * parsing of an optional YAML block+-- * strikeout using @~~this~~@ syntax+-- * superscript using @^this^@ syntax+-- * subscript using @~this~@ syntax+-- * automatic assignment of ids to headers+-- * PHP-style footnotes, e.g. @[^1]@ (NOT YET)+-- * “pipe” tables (as used on GitHub) (NOT YET)+--+-- One do not need to enable or tweak anything for these to work, they are+-- built-in features.+--+-- The readme contains a more detailed description of differences between+-- Common Mark and MMark.+--+-- === How to use the library+--+-- The module is intended to be imported qualified:+--+-- > import Text.MMark (MMark)+-- > import qualified Text.MMark as MMark+--+-- Working with MMark happens in three stages:+--+-- 1. Parsing of markdown document.+-- 2. Applying extensions, which optionally may require scanning of+-- previously parsed document (for example to build a table of+-- contents).+-- 3. Rendering of HTML document.+--+-- The structure of the documentation below corresponds to these stages and+-- should clarify the details.+--+-- === Other modules of interest+--+-- This module contains all the “core” functionality you may need. However,+-- one of the main selling points of MMark is that it's possible to write+-- your own extensions which stay highly composable (if done right), so+-- proliferation of third-party extensions is to be expected and encouraged.+-- To write an extension of your own import the "Text.MMark.Extension"+-- module, which has some documentation focusing on extension writing.++module Text.MMark+ ( -- * Parsing+ MMark+ , MMarkErr (..)+ , parse+ , parseErrorsPretty+ -- * Extensions+ , Extension+ , useExtension+ , useExtensions+ -- * Scanning+ , runScanner+ , projectYaml+ -- * Rendering+ , render )+where++import Data.Aeson+import Data.List.NonEmpty (NonEmpty (..))+import Data.Text (Text)+import Text.MMark.Internal+import Text.MMark.Parser+import Text.Megaparsec (ParseError (..), parseErrorPretty_, mkPos)++-- | Extract contents of optional YAML block that may have been parsed.++projectYaml :: MMark -> Maybe Value+projectYaml = mmarkYaml++-- | Pretty-print a collection of parse errors returned from 'parse'.+--+-- __Pro tip__: if you would like to pretty-print a single 'ParseError', use+-- @'parseErrorPretty_' ('mkPos' 4)@, because Common Mark suggests that we+-- should assume tab width 4, and that's what we do in the parser.++parseErrorsPretty+ :: Text -- ^ Original input for parser+ -> NonEmpty (ParseError Char MMarkErr) -- ^ Collection of parse errors+ -> String -- ^ Result of pretty-printing+parseErrorsPretty input = concatMap (parseErrorPretty_ (mkPos 4) input)
+ Text/MMark/Extension.hs view
@@ -0,0 +1,113 @@+-- |+-- Module : Text.MMark.Extension+-- Copyright : © 2017 Mark Karpov+-- License : BSD 3 clause+--+-- Maintainer : Mark Karpov <markkarpov92@gmail.com>+-- Stability : experimental+-- Portability : portable+--+-- This module provides building blocks for extension creation.+--+-- We suggest using a qualified import, like this:+--+-- > import Text.MMark.Extension (Bni, Block (..), Inline (..))+-- > import qualified Text.MMark.Extension as Ext+--+-- === Details about extensions+--+-- There are four kinds of extension-producing functions. They correspond+-- internally to four functions that are applied to the parsed document in+-- turn:+--+-- * 'blockTrans' is applied first, as it's quite general and can change+-- block-level structure of document as well as inline-level+-- structure.+-- * 'inlineTrans' is applied to every inline in the document obtained+-- in the previous step.+-- * 'inlineRender' is applied to every inline; this function produces+-- HTML rendition of the inlines and we also preserve the original+-- inline so 'blockRender' can look at it (sometimes it is useful).+-- * 'blockRender' is applied to every block to obtain HTML rendition of+-- the whole document.+--+-- When one combines different extensions, extensions of the same kind get+-- fused together into a single function. This allows for faster processing+-- in the end.++{-# LANGUAGE RankNTypes #-}++module Text.MMark.Extension+ ( -- * Extension construction+ Extension+ -- ** Block-level manipulation+ , Bni+ , Block (..)+ , blockTrans+ , blockRender+ , Ois+ , getOis+ -- ** Inline-level manipulation+ , Inline (..)+ , inlineTrans+ , inlineRender+ -- * Scanner construction+ , scanner+ -- * Utils+ , asPlainText+ , headerId+ , headerFragment )+where++import Data.Monoid hiding ((<>))+import Lucid+import Text.MMark.Internal+import qualified Control.Foldl as L++-- | Create an extension that performs a transformation on 'Block's of+-- markdown document.++blockTrans :: (Bni -> Bni) -> Extension+blockTrans f = mempty { extBlockTrans = Endo f }++-- | Create an extension that replaces or augments rendering of 'Block's of+-- markdown document. The argument of 'blockRender' will be given the+-- rendering function constructed so far @'Block' ('Ois', 'Html' ()) ->+-- 'Html' ()@ as well as an actual block to render—@'Block' ('Ois', 'Html'+-- ())@. The user can then decide whether to replace\/reuse that function to+-- get the final rendering of the type @'Html' ()@.+--+-- The argument of 'blockRender' can also be thought of as a function that+-- transforms the rendering function constructed so far:+--+-- > (Block (Ois, Html ()) -> Html ()) -> (Block (Ois, Html ()) -> Html ())+--+-- See also: 'Ois' and 'getOis'.++blockRender+ :: ((Block (Ois, Html ()) -> Html ()) -> Block (Ois, Html ()) -> Html ())+ -> Extension+blockRender f = mempty { extBlockRender = Render f }++-- | Create an extension that performs a transformation on 'Inline'+-- components in entire markdown document.++inlineTrans :: (Inline -> Inline) -> Extension+inlineTrans f = mempty { extInlineTrans = Endo f }++-- | Create an extension that replaces or augments rendering of 'Inline's of+-- markdown document. This works like 'blockRender'.++inlineRender+ :: ((Inline -> Html ()) -> Inline -> Html ())+ -> Extension+inlineRender f = mempty { extInlineRender = Render f }++-- | Create a 'L.Fold' from an initial state and a folding function.++scanner+ :: a -- ^ Initial state+ -> (a -> Bni -> a) -- ^ Folding function+ -> L.Fold Bni a -- ^ Resulting 'L.Fold'+scanner a f = L.Fold f a id+{-# INLINE scanner #-}
+ Text/MMark/Internal.hs view
@@ -0,0 +1,419 @@+-- |+-- Module : Text.MMark.Internal+-- Copyright : © 2017 Mark Karpov+-- License : BSD 3 clause+--+-- Maintainer : Mark Karpov <markkarpov92@gmail.com>+-- Stability : experimental+-- Portability : portable+--+-- Internal definitions.++{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}++module Text.MMark.Internal+ ( -- * Types+ MMark (..)+ , Extension (..)+ , Bni+ , Block (..)+ , Inline (..)+ -- * Extensions+ , runScanner+ , useExtension+ , useExtensions+ -- * Renders+ , render+ , Ois+ , getOis+ , Render (..)+ , defaultBlockRender+ , defaultInlineRender+ -- * Utils+ , asPlainText+ , headerId+ , headerFragment )+where++import Control.Arrow+import Control.DeepSeq+import Control.Monad+import Data.Aeson+import Data.Char (isSpace)+import Data.Data (Data)+import Data.Function (on)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Monoid hiding ((<>))+import Data.Semigroup+import Data.Text (Text)+import Data.Typeable (Typeable)+import GHC.Generics+import Lucid+import Text.URI (URI (..))+import qualified Control.Foldl as L+import qualified Data.Text as T+import qualified Text.URI as URI++----------------------------------------------------------------------------+-- Types++-- | Representation of complete markdown document. You can't look inside of+-- 'MMark' on purpose. The only way to influence an 'MMark' document you+-- obtain as a result of parsing is via the extension mechanism.++data MMark = MMark+ { mmarkYaml :: Maybe Value+ -- ^ Parsed YAML document at the beginning (optional)+ , mmarkBlocks :: [Bni]+ -- ^ Actual contents of the document+ , mmarkExtension :: Extension+ -- ^ Extension specifying how to process and render the blocks+ }++instance NFData MMark where+ rnf MMark {..} = rnf mmarkYaml `seq` rnf mmarkBlocks++-- | An extension. You can apply extensions with 'useExtension' and+-- 'useExtensions' functions. The "Text.MMark.Extension" module provides+-- tools for extension creation.+--+-- Note that 'Extension' is an instance of 'Semigroup' and 'Monoid', i.e.+-- you can combine several extensions into one. Since the @('<>')@ operator+-- is right-associative and 'mconcat' is a right fold under the hood, the+-- expression+--+-- > l <> r+--+-- means that the extension @r@ will be applied before the extension @l@,+-- similar to how 'Endo' works. This may seem counter-intuitive, but only+-- with this logic we get consistency of ordering with more complex+-- expressions:+--+-- > e2 <> e1 <> e0 == e2 <> (e1 <> e0)+--+-- Here, @e0@ will be applied first, then @e1@, then @e2@. The same applies+-- to expressions involving 'mconcat'—extensions closer to beginning of the+-- list passed to 'mconcat' will be applied later.++data Extension = Extension+ { extBlockTrans :: Endo Bni+ -- ^ Block transformation+ , extBlockRender :: Render (Block (Ois, Html ()))+ -- ^ Block render+ , extInlineTrans :: Endo Inline+ -- ^ Inline transformation+ , extInlineRender :: Render Inline+ -- ^ Inline render+ }++instance Semigroup Extension where+ x <> y = Extension+ { extBlockTrans = on (<>) extBlockTrans x y+ , extBlockRender = on (<>) extBlockRender x y+ , extInlineTrans = on (<>) extInlineTrans x y+ , extInlineRender = on (<>) extInlineRender x y }++instance Monoid Extension where+ mempty = Extension+ { extBlockTrans = mempty+ , extBlockRender = mempty+ , extInlineTrans = mempty+ , extInlineRender = mempty }+ mappend = (<>)++-- | A shortcut for the frequently used type @'Block' ('NonEmpty'+-- 'Inline')@.++type Bni = Block (NonEmpty Inline)++-- | We can think of a markdown document as a collection of+-- blocks—structural elements like paragraphs, block quotations, lists,+-- headings, thematic breaks, and code blocks. Some blocks (like block+-- quotes and list items) contain other blocks; others (like headings and+-- paragraphs) contain inline content, see 'Inline'.+--+-- We can divide blocks into two types: container blocks, which can contain+-- other blocks, and leaf blocks, which cannot.++data Block a+ = ThematicBreak+ -- ^ Thematic break, leaf block+ | Heading1 a+ -- ^ Heading (level 1), leaf block+ | Heading2 a+ -- ^ Heading (level 2), leaf block+ | Heading3 a+ -- ^ Heading (level 3), leaf block+ | Heading4 a+ -- ^ Heading (level 4), leaf block+ | Heading5 a+ -- ^ Heading (level 5), leaf block+ | Heading6 a+ -- ^ Heading (level 6), leaf block+ | CodeBlock (Maybe Text) Text+ -- ^ Code block, leaf block with info string and contents+ | Paragraph a+ -- ^ Paragraph, leaf block+ | Blockquote [Block a]+ -- ^ Blockquote container block+ | OrderedList (NonEmpty [Block a])+ -- ^ Ordered list, container block+ | UnorderedList (NonEmpty [Block a])+ -- ^ Unordered list, container block+ | Naked a+ -- ^ Naked content, without an enclosing tag+ deriving (Show, Eq, Ord, Data, Typeable, Generic, Functor, Foldable)++instance NFData a => NFData (Block a)++-- | Inline markdown content.++data Inline+ = Plain Text+ -- ^ Plain text+ | LineBreak+ -- ^ Line break (hard)+ | Emphasis (NonEmpty Inline)+ -- ^ Emphasis+ | Strong (NonEmpty Inline)+ -- ^ Strong emphasis+ | Strikeout (NonEmpty Inline)+ -- ^ Strikeout+ | Subscript (NonEmpty Inline)+ -- ^ Subscript+ | Superscript (NonEmpty Inline)+ -- ^ Superscript+ | CodeSpan Text+ -- ^ Code span+ | Link (NonEmpty Inline) URI (Maybe Text)+ -- ^ Link with text, destination, and optionally title+ | Image (NonEmpty Inline) URI (Maybe Text)+ -- ^ Image with description, URL, and optionally title+ deriving (Show, Eq, Ord, Data, Typeable, Generic)++instance NFData Inline++----------------------------------------------------------------------------+-- Extensions++-- | Apply an 'Extension' to an 'MMark' document. The order in which you+-- apply 'Extension's /does matter/. Extensions you apply first take effect+-- first. The extension system is designed in such a way that in many cases+-- the order doesn't matter, but sometimes the difference is important.++useExtension :: Extension -> MMark -> MMark+useExtension ext mmark =+ mmark { mmarkExtension = ext <> mmarkExtension mmark }++-- | Apply several 'Extension's to an 'MMark' document.+--+-- This is a simple shortcut:+--+-- > useExtensions exts = useExtension (mconcat exts)+--+-- As mentioned in the docs for 'useExtension', the order in which you apply+-- extensions matters. Extensions closer to beginning of the list are+-- applied later, i.e. the last extension in the list is applied first.++useExtensions :: [Extension] -> MMark -> MMark+useExtensions exts = useExtension (mconcat exts)++-- | Scan an 'MMark' document efficiently in one pass. This uses the+-- excellent 'L.Fold' type, which see.+--+-- Take a look at the "Text.MMark.Extension" module if you want to create+-- scanners of your own.++runScanner+ :: MMark -- ^ Document to scan+ -> L.Fold Bni a -- ^ 'L.Fold' to use+ -> a -- ^ Result of scanning+runScanner MMark {..} f = L.fold f mmarkBlocks+{-# INLINE runScanner #-}++----------------------------------------------------------------------------+-- Renders++-- | Render a 'MMark' markdown document. You can then render @'Html' ()@ to+-- various things:+--+-- * to lazy 'Data.Taxt.Lazy.Text' with 'renderText'+-- * to lazy 'Data.ByteString.Lazy.ByteString' with 'renderBS'+-- * directly to file with 'renderToFile'++render :: MMark -> Html ()+render MMark {..} =+ mapM_ produceBlock mmarkBlocks+ where+ Extension {..} = mmarkExtension+ produceBlock = applyBlockRender extBlockRender+ . fmap ((Ois &&& mapM_ (applyInlineRender extInlineRender)) .+ fmap (appEndo extInlineTrans))+ . appEndo extBlockTrans++-- | A wrapper for “originial inlines”. Source inlines are wrapped in this+-- during rendering of inline components and then it's available to block+-- render, but only for inspection. Altering of 'Ois' is not possible+-- because the user cannot construct a value of the 'Ois' type, she can only+-- inspect it with 'getOis'.++newtype Ois = Ois (NonEmpty Inline)++-- | Project @'NonEmpty' 'Inline'@ from 'Ois'.++getOis :: Ois -> NonEmpty Inline+getOis (Ois inlines) = inlines++-- | An internal type that captures the extensible rendering process we use.+-- 'Render' has a function inside which transforms a rendering function of+-- the type @a -> Html ()@.++newtype Render a = Render+ { getRender :: (a -> Html ()) -> a -> Html () }++instance Semigroup (Render a) where+ Render f <> Render g = Render $ \h -> f (g h)++instance Monoid (Render a) where+ mempty = Render id+ mappend = (<>)++-- | Apply a 'Render' to a given @'Block' 'Html' ()@.++applyBlockRender+ :: Render (Block (Ois, Html ()))+ -> Block (Ois, Html ())+ -> Html ()+applyBlockRender r = getRender r defaultBlockRender++-- | The default 'Block' render. Note that it does not care about what we+-- have rendered so far because it always starts rendering. Thus it's OK to+-- just pass it something dummy as the second argument of the inner+-- function.++defaultBlockRender :: Block (Ois, Html ()) -> Html ()+defaultBlockRender = \case+ ThematicBreak ->+ hr_ [] >> newline+ Heading1 (h,html) ->+ h1_ (mkId h) html >> newline+ Heading2 (h,html) ->+ h2_ (mkId h) html >> newline+ Heading3 (h,html) ->+ h3_ (mkId h) html >> newline+ Heading4 (h,html) ->+ h4_ (mkId h) html >> newline+ Heading5 (h,html) ->+ h5_ (mkId h) html >> newline+ Heading6 (h,html) ->+ h6_ (mkId h) html >> newline+ CodeBlock infoString txt -> do+ let f x = class_ $ "language-" <> T.takeWhile (not . isSpace) x+ pre_ $ code_ (maybe [] (pure . f) infoString) (toHtml txt)+ newline+ Paragraph (_,html) ->+ p_ html >> newline+ Blockquote blocks ->+ blockquote_ (mapM_ defaultBlockRender blocks)+ OrderedList items -> do+ ol_ $ do+ newline+ forM_ items $ \x -> do+ li_ (mapM_ defaultBlockRender x)+ newline+ newline+ UnorderedList items -> do+ ul_ $ do+ newline+ forM_ items $ \x -> do+ li_ (mapM_ defaultBlockRender x)+ newline+ newline+ Naked (_,html) ->+ html+ where+ mkId (Ois x) = [id_ (headerId x)]++-- | Apply a render to a given 'Inline'.++applyInlineRender :: Render Inline -> Inline -> Html ()+applyInlineRender r = getRender r defaultInlineRender++-- | The default render for 'Inline' elements. Comments about+-- 'defaultBlockRender' apply here just as well.++defaultInlineRender :: Inline -> Html ()+defaultInlineRender = \case+ Plain txt ->+ toHtml txt+ LineBreak ->+ br_ [] >> newline+ Emphasis inner ->+ em_ (mapM_ defaultInlineRender inner)+ Strong inner ->+ strong_ (mapM_ defaultInlineRender inner)+ Strikeout inner ->+ del_ (mapM_ defaultInlineRender inner)+ Subscript inner ->+ sub_ (mapM_ defaultInlineRender inner)+ Superscript inner ->+ sup_ (mapM_ defaultInlineRender inner)+ CodeSpan txt ->+ code_ (toHtmlRaw txt)+ Link inner dest mtitle ->+ let title = maybe [] (pure . title_) mtitle+ in a_ (href_ (URI.render dest) : title) (mapM_ defaultInlineRender inner)+ Image desc src mtitle ->+ let title = maybe [] (pure . title_) mtitle+ in img_ (alt_ (asPlainText desc) : src_ (URI.render src) : title)++-- | HTML containing a newline.++newline :: Html ()+newline = "\n"++----------------------------------------------------------------------------+-- Utils++-- | Convert a non-empty collection of 'Inline's into their plain text+-- representation. This is used e.g. to render image descriptions.++asPlainText :: NonEmpty Inline -> Text+asPlainText = foldMap $ \case+ Plain txt -> txt+ LineBreak -> "\n"+ Emphasis xs -> asPlainText xs+ Strong xs -> asPlainText xs+ Strikeout xs -> asPlainText xs+ Subscript xs -> asPlainText xs+ Superscript xs -> asPlainText xs+ CodeSpan txt -> txt+ Link xs _ _ -> asPlainText xs+ Image xs _ _ -> asPlainText xs++-- | Generate value of id attribute for a given header. This is used during+-- rendering and also can be used to get id of a header for linking to it in+-- extensions.+--+-- See also: 'headerFragment'.++headerId :: NonEmpty Inline -> Text+headerId = T.intercalate "-" . T.words . T.toLower . asPlainText++-- | Generate a 'URI' with just fragment from its textual representation.+-- Useful for getting URL from id of a header.++headerFragment :: Text -> URI+headerFragment fragment = URI+ { uriScheme = Nothing+ , uriAuthority = Left False+ , uriPath = []+ , uriQuery = []+ , uriFragment = URI.mkFragment fragment }
+ Text/MMark/Parser.hs view
@@ -0,0 +1,762 @@+-- |+-- Module : Text.MMark.Parser+-- Copyright : © 2017 Mark Karpov+-- License : BSD 3 clause+--+-- Maintainer : Mark Karpov <markkarpov92@gmail.com>+-- Stability : experimental+-- Portability : portable+--+-- MMark parser.++{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Text.MMark.Parser+ ( MMarkErr (..)+ , parse )+where++import Control.Applicative+import Control.DeepSeq+import Control.Monad+import Control.Monad.State.Strict+import Data.Bifunctor (Bifunctor (..))+import Data.Data (Data)+import Data.Default.Class+import Data.List.NonEmpty (NonEmpty (..), (<|))+import Data.Maybe (isNothing, fromJust, fromMaybe)+import Data.Semigroup ((<>))+import Data.Text (Text)+import Data.Typeable (Typeable)+import Data.Void+import GHC.Generics+import Text.MMark.Internal+import Text.Megaparsec hiding (parse)+import Text.Megaparsec.Char hiding (eol)+import Text.URI (URI)+import qualified Control.Applicative.Combinators.NonEmpty as NE+import qualified Data.Char as Char+import qualified Data.List.NonEmpty as NE+import qualified Data.Set as E+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Yaml as Yaml+import qualified Text.Email.Validate as Email+import qualified Text.Megaparsec.Char.Lexer as L+import qualified Text.URI as URI++----------------------------------------------------------------------------+-- Data types++-- | Parser type we use internally.++type Parser = Parsec MMarkErr Text++-- | MMark custom parse errors.++data MMarkErr+ = YamlParseError String+ -- ^ YAML error that occurred during parsing of a YAML block+ | NonFlankingDelimiterRun (NonEmpty Char)+ -- ^ This delimiter run should be in left- or right- flanking position+ deriving (Eq, Ord, Show, Read, Generic, Typeable, Data)++instance ShowErrorComponent MMarkErr where+ showErrorComponent = \case+ YamlParseError str ->+ "YAML parse error: " ++ str+ NonFlankingDelimiterRun dels ->+ showTokens dels ++ " should be in left- or right- flanking position"++instance NFData MMarkErr++-- | Parser type for inlines.++type IParser = StateT CharType (Parsec MMarkErr Text)++-- | 'Inline' source pending parsing.++data Isp = Isp SourcePos Text+ deriving (Eq, Ord, Show)++-- | Type of last seen character.++data CharType+ = SpaceChar -- ^ White space+ | LeftFlankingDel -- ^ Left flanking delimiter+ | RightFlankingDel -- ^ Right flaking delimiter+ | OtherChar -- ^ Other character+ deriving (Eq, Ord, Show)++-- | Frame that describes where we are in parsing inlines.++data InlineFrame+ = EmphasisFrame -- ^ Emphasis with asterisk @*@+ | EmphasisFrame_ -- ^ Emphasis with underscore @_@+ | StrongFrame -- ^ Strong emphasis with asterisk @**@+ | StrongFrame_ -- ^ Strong emphasis with underscore @__@+ | StrikeoutFrame -- ^ Strikeout+ | SubscriptFrame -- ^ Subscript+ | SuperscriptFrame -- ^ Superscript+ deriving (Eq, Ord, Show)++-- | State of inline parsing that specifies whether we expect to close one+-- frame or there is a possibility to close one of two alternatives.++data InlineState+ = SingleFrame InlineFrame -- ^ One frame to be closed+ | DoubleFrame InlineFrame InlineFrame -- ^ Two frames to be closed+ deriving (Eq, Ord, Show)++-- | Configuration in inline parser.++data InlineConfig = InlineConfig+ { iconfigAllowEmpty :: !Bool+ -- ^ Whether to accept empty inline blocks+ , iconfigAllowLinks :: !Bool+ -- ^ Whether to parse links+ , iconfigAllowImages :: !Bool+ -- ^ Whether to parse images+ }++instance Default InlineConfig where+ def = InlineConfig+ { iconfigAllowEmpty = True+ , iconfigAllowLinks = True+ , iconfigAllowImages = True+ }++-- | A shortcut type synonym for sub-parsers that may fail and we can+-- recover from the failure.++type E = Either (ParseError Char MMarkErr)++----------------------------------------------------------------------------+-- Block parser++-- | Parse a markdown document in the form of a strict 'Text' value and+-- either report parse errors or return a 'MMark' document. Note that the+-- parser has the ability to report multiple parse errors at once.++parse+ :: String+ -- ^ File name (only to be used in error messages), may be empty+ -> Text+ -- ^ Input to parse+ -> Either (NonEmpty (ParseError Char MMarkErr)) MMark+ -- ^ Parse errors or parsed document+parse file input =+ case runParser ((,) <$> optional pYamlBlock <*> pBlocks) file input of+ -- NOTE This parse error only happens when document structure on block+ -- level cannot be parsed even with recovery, which should not normally+ -- happen.+ Left err -> Left (nes err)+ Right (myaml, blocks) ->+ let parsed = doInline <$> blocks+ doInline = \case+ Left err -> Naked (Left err)+ Right x -> first (replaceEof "end of inline block")+ . runIsp (pInlines def <* eof) <$> x+ getErrs (Left e) es = e : es+ getErrs _ es = es+ fromRight (Right x) = x+ fromRight _ =+ error "Text.MMark.Parser.parse: the impossible happened"+ in case NE.nonEmpty (foldMap (foldr getErrs []) parsed) of+ Nothing -> Right MMark+ { mmarkYaml = myaml+ , mmarkBlocks = fmap fromRight <$> parsed+ , mmarkExtension = mempty }+ Just es -> Left es++pYamlBlock :: Parser Yaml.Value+pYamlBlock = do+ dpos <- getPosition+ string "---" *> sc' *> eol+ let go = do+ l <- takeWhileP Nothing notNewline+ void (optional eol)+ e <- atEnd+ if e || T.stripEnd l == "---"+ then return []+ else (l :) <$> go+ ls <- go+ case (Yaml.decodeEither . TE.encodeUtf8 . T.intercalate "\n") ls of+ Left err' -> do+ let (apos, err) = splitYamlError (sourceName dpos) err'+ setPosition (fromMaybe dpos apos)+ (fancyFailure . E.singleton . ErrorCustom . YamlParseError) err+ Right v ->+ return v++pBlocks :: Parser [E (Block Isp)]+pBlocks = do+ setTabWidth (mkPos 4)+ sc *> manyTill pBlock eof++pBlock :: Parser (E (Block Isp))+pBlock = choice+ [ try (pure <$> pThematicBreak)+ , pAtxHeading+ , pure <$> pFencedCodeBlock+ , try (pure <$> pIndentedCodeBlock)+ , pure <$> pParagraph ]++pThematicBreak :: Parser (Block Isp)+pThematicBreak = do+ void casualLevel+ l <- lookAhead nonEmptyLine+ if isThematicBreak l+ then ThematicBreak <$ nonEmptyLine <* sc+ else empty++pAtxHeading :: Parser (E (Block Isp))+pAtxHeading = do+ (void . lookAhead . try) start+ withRecovery recover $ do+ hlevel <- length <$> start+ sc1'+ ispPos <- getPosition+ r <- someTill (satisfy notNewline <?> "heading character") . try $+ optional (sc1' *> some (char '#') *> sc') *> (eof <|> eol)+ let toBlock = case hlevel of+ 1 -> Heading1+ 2 -> Heading2+ 3 -> Heading3+ 4 -> Heading4+ 5 -> Heading5+ _ -> Heading6+ (Right . toBlock) (Isp ispPos (T.strip (T.pack r))) <$ sc+ where+ start = casualLevel *> count' 1 6 (char '#')+ recover err =+ Left err <$ takeWhileP Nothing notNewline <* optional eol++pFencedCodeBlock :: Parser (Block Isp)+pFencedCodeBlock = do+ level <- casualLevel+ let p ch = try $ do+ void $ count 3 (char ch)+ n <- (+ 3) . length <$> many (char ch)+ ml <- optional (T.strip <$> someEscapedWith notNewline <?> "info string")+ guard (maybe True (not . T.any (== '`')) ml)+ return+ (ch, n,+ case ml of+ Nothing -> Nothing+ Just l ->+ if T.null l+ then Nothing+ else Just l)+ (ch, n, infoString) <- (p '`' <|> p '~') <* eol+ let content = label "code block content" (option "" nonEmptyLine <* eol)+ closingFence = try . label "closing code fence" $ do+ void casualLevel'+ void $ count n (char ch)+ (void . many . char) ch+ sc'+ eof <|> eol+ ls <- manyTill content closingFence+ CodeBlock infoString (assembleCodeBlock level ls) <$ sc++pIndentedCodeBlock :: Parser (Block Isp)+pIndentedCodeBlock = do+ initialIndent <- codeBlockLevel+ let go ls = do+ immediate <- lookAhead (True <$ try codeBlockLevel' <|> pure False)+ eventual <- lookAhead (True <$ try codeBlockLevel <|> pure False)+ if not immediate && not eventual+ then return ls+ else do+ l <- option "" nonEmptyLine+ continue <- eol'+ if continue+ then go (l:ls)+ else return (l:ls)+ -- NOTE This is a bit unfortunate, but it's difficult to guarantee+ -- that preceding space is not yet consumed when we get to+ -- interpreting input as an indented code block, so we need to restore+ -- the space this way.+ f x = T.replicate (unPos initialIndent - 1) " " <> x+ g [] = []+ g (x:xs) = f x : xs+ ls <- g . reverse . dropWhile isBlank <$> go []+ CodeBlock Nothing (assembleCodeBlock (mkPos 5) ls) <$ sc++pParagraph :: Parser (Block Isp)+pParagraph = do+ void casualLevel+ startPos <- getPosition+ let go = do+ ml <- lookAhead (optional nonEmptyLine)+ case ml of+ Nothing -> return []+ Just l ->+ if isBlank l+ then return []+ else do+ void nonEmptyLine+ continue <- eol'+ (l :) <$> if continue then go else return []+ l <- nonEmptyLine+ continue <- eol'+ ls <- if continue then go else return []+ Paragraph (Isp startPos (assembleParagraph (l:ls))) <$ sc++----------------------------------------------------------------------------+-- Inline parser++-- | Run a given parser on 'Isp'.++runIsp+ :: IParser a -- ^ The parser to run+ -> Isp -- ^ Input for the parser+ -> Either (ParseError Char MMarkErr) a -- ^ Result of parsing+runIsp p (Isp startPos input) =+ snd (runParser' (evalStateT p SpaceChar) pst)+ where+ pst = State+ { stateInput = input+ , statePos = nes startPos+ , stateTokensProcessed = 0+ , stateTabWidth = mkPos 4 }++pInlines :: InlineConfig -> IParser (NonEmpty Inline)+pInlines InlineConfig {..} =+ if iconfigAllowEmpty+ then nes (Plain "") <$ eof <|> stuff+ else stuff+ where+ stuff = NE.some . label "inline content" . choice $+ [ pCodeSpan ] <>+ [ pInlineLink | iconfigAllowLinks ] <>+ [ pImage | iconfigAllowImages ] <>+ [ try (angel pAutolink) | iconfigAllowLinks ] <>+ [ pEnclosedInline+ , try pHardLineBreak+ , pPlain ]+ angel = between (char '<') (char '>')++pCodeSpan :: IParser Inline+pCodeSpan = do+ n <- try (length <$> some (char '`'))+ let finalizer = try $ do+ void $ count n (char '`')+ notFollowedBy (char '`')+ r <- CodeSpan . collapseWhiteSpace . T.concat <$>+ manyTill (label "code span content" $+ takeWhile1P Nothing (== '`') <|>+ takeWhile1P Nothing (/= '`'))+ finalizer+ put OtherChar+ return r++pInlineLink :: IParser Inline+pInlineLink = do+ xs <- between (char '[') (char ']') $+ pInlines def { iconfigAllowLinks = False }+ void (char '(') <* sc+ dest <- pUri+ mtitle <- optional (sc1 *> pTitle)+ sc <* char ')'+ put OtherChar+ return (Link xs dest mtitle)++pImage :: IParser Inline+pImage = do+ let nonEmptyDesc = char '!' *> between (char '[') (char ']')+ (pInlines def { iconfigAllowImages = False })+ alt <- nes (Plain "") <$ string "![]" <|> nonEmptyDesc+ void (char '(') <* sc+ src <- pUri+ mtitle <- optional (sc1 *> pTitle)+ sc <* char ')'+ put OtherChar+ return (Image alt src mtitle)++pUri :: IParser URI+pUri = do+ uri <- between (char '<') (char '>') URI.parser <|> naked+ put OtherChar+ return uri+ where+ naked = do+ startPos <- getPosition+ input <- takeWhileP Nothing $ \x ->+ not (isSpaceN x || x == ')')+ let pst = State+ { stateInput = input+ , statePos = nes startPos+ , stateTokensProcessed = 0+ , stateTabWidth = mkPos 4 }+ case snd (runParser' (URI.parser <* eof) pst) of+ Left err' ->+ case replaceEof "end of URI literal" err' of+ TrivialError pos us es -> do+ setPosition (NE.head pos)+ failure us es+ FancyError pos xs -> do+ setPosition (NE.head pos)+ fancyFailure xs+ Right x -> return x++pTitle :: IParser Text+pTitle = choice+ [ p '\"' '\"'+ , p '\'' '\''+ , p '(' ')' ]+ where+ p start end = between (char start) (char end) $+ manyEscapedWith (/= end) "unescaped character"++pAutolink :: IParser Inline+pAutolink = do+ notFollowedBy (char '>') -- empty links don't make sense+ uri <- URI.parser+ put OtherChar+ return $ case isEmailUri uri of+ Nothing ->+ let txt = (nes . Plain . URI.render) uri+ in Link txt uri Nothing+ Just email ->+ let txt = nes (Plain email)+ uri' = URI.makeAbsolute mailtoScheme uri+ in Link txt uri' Nothing++pEnclosedInline :: IParser Inline+pEnclosedInline = do+ let noEmpty = def { iconfigAllowEmpty = False }+ st <- choice+ [ pLfdr (DoubleFrame StrongFrame StrongFrame)+ , pLfdr (DoubleFrame StrongFrame EmphasisFrame)+ , pLfdr (SingleFrame StrongFrame)+ , pLfdr (SingleFrame EmphasisFrame)+ , pLfdr (DoubleFrame StrongFrame_ StrongFrame_)+ , pLfdr (DoubleFrame StrongFrame_ EmphasisFrame_)+ , pLfdr (SingleFrame StrongFrame_)+ , pLfdr (SingleFrame EmphasisFrame_)+ , pLfdr (DoubleFrame StrikeoutFrame StrikeoutFrame)+ , pLfdr (DoubleFrame StrikeoutFrame SubscriptFrame)+ , pLfdr (SingleFrame StrikeoutFrame)+ , pLfdr (SingleFrame SubscriptFrame)+ , pLfdr (SingleFrame SuperscriptFrame) ]+ case st of+ SingleFrame x ->+ liftFrame x <$> pInlines noEmpty <* pRfdr x+ DoubleFrame x y -> do+ inlines0 <- pInlines noEmpty+ thisFrame <- pRfdr x <|> pRfdr y+ let thatFrame = if x == thisFrame then y else x+ immediate <- True <$ pRfdr thatFrame <|> pure False+ if immediate+ then (return . liftFrame thatFrame . nes . liftFrame thisFrame) inlines0+ else do+ inlines1 <- pInlines noEmpty+ void (pRfdr thatFrame)+ return . liftFrame thatFrame $+ liftFrame thisFrame inlines0 <| inlines1++pLfdr :: InlineState -> IParser InlineState+pLfdr st = try $ do+ let dels = inlineStateDel st+ mpos <- getNextTokenPosition+ void (string dels)+ leftChar <- get+ mrightChar <- lookAhead (optional anyChar)+ let failNow = do+ forM_ mpos setPosition+ (mmarkErr . NonFlankingDelimiterRun . toNesTokens) dels+ case (leftChar, isTransparent <$> mrightChar) of+ (_, Nothing) -> failNow+ (_, Just True) -> failNow+ (RightFlankingDel, _) -> failNow+ (OtherChar, _) -> failNow+ (SpaceChar, _) -> return ()+ (LeftFlankingDel, _) -> return ()+ put LeftFlankingDel+ return st++pRfdr :: InlineFrame -> IParser InlineFrame+pRfdr frame = try $ do+ let dels = inlineFrameDel frame+ mpos <- getNextTokenPosition+ void (string dels)+ leftChar <- get+ mrightChar <- lookAhead (optional anyChar)+ let failNow = do+ forM_ mpos setPosition+ (mmarkErr . NonFlankingDelimiterRun . toNesTokens) dels+ case (leftChar, mrightChar) of+ (SpaceChar, _) -> failNow+ (LeftFlankingDel, _) -> failNow+ (_, Nothing) -> return ()+ (_, Just rightChar) ->+ if | isTransparent rightChar -> return ()+ | isMarkupChar rightChar -> return ()+ | otherwise -> failNow+ put RightFlankingDel+ return frame++pHardLineBreak :: IParser Inline+pHardLineBreak = do+ void (char '\\')+ eol+ notFollowedBy eof+ sc'+ put SpaceChar+ return LineBreak++pPlain :: IParser Inline+pPlain = Plain . T.pack <$> some+ (pEscapedChar <|> pNewline <|> pNonEscapedChar)+ where+ pEscapedChar = escapedChar <* put OtherChar+ pNewline = hidden . try $+ '\n' <$ sc' <* eol <* sc' <* put SpaceChar+ pNonEscapedChar = label "unescaped non-markup character" . choice $+ [ try (char '\\' <* notFollowedBy eol) <* put OtherChar+ , try (char '!' <* notFollowedBy (char '[')) <* put SpaceChar+ , try (char '<' <* notFollowedBy (pAutolink <* char '>')) <* put OtherChar+ , spaceChar <* put SpaceChar+ , satisfy isTrans <* put SpaceChar+ , satisfy isOther <* put OtherChar ]+ isTrans x = isTransparentPunctuation x && x /= '!'+ isOther x = not (isMarkupChar x) && x /= '\\' && x /= '!' && x /= '<'++----------------------------------------------------------------------------+-- Parsing helpers++casualLevel :: Parser Pos+casualLevel = L.indentGuard sc LT (mkPos 5)++casualLevel' :: Parser Pos+casualLevel' = L.indentGuard sc' LT (mkPos 5)++codeBlockLevel :: Parser Pos+codeBlockLevel = L.indentGuard sc GT (mkPos 4)++codeBlockLevel' :: Parser Pos+codeBlockLevel' = L.indentGuard sc' GT (mkPos 4)++nonEmptyLine :: Parser Text+nonEmptyLine = takeWhile1P Nothing notNewline++manyEscapedWith :: MonadParsec e Text m => (Char -> Bool) -> String -> m Text+manyEscapedWith f l = T.pack <$> many (escapedChar <|> (satisfy f <?> l))++someEscapedWith :: MonadParsec e Text m => (Char -> Bool) -> m Text+someEscapedWith f = T.pack <$> some (escapedChar <|> satisfy f)++escapedChar :: MonadParsec e Text m => m Char+escapedChar = try (char '\\' *> satisfy isAsciiPunctuation)+ <?> "escaped character"++sc :: MonadParsec e Text m => m ()+sc = void $ takeWhileP (Just "white space") isSpaceN++sc1 :: MonadParsec e Text m => m ()+sc1 = void $ takeWhile1P (Just "white space") isSpaceN++sc' :: MonadParsec e Text m => m ()+sc' = void $ takeWhileP (Just "white space") isSpace++sc1' :: MonadParsec e Text m => m ()+sc1' = void $ takeWhile1P (Just "white space") isSpace++eol :: MonadParsec e Text m => m ()+eol = void . label "newline" $ choice+ [ string "\n"+ , string "\r\n"+ , string "\r" ]++eol' :: MonadParsec e Text m => m Bool+eol' = option False (True <$ eol)++----------------------------------------------------------------------------+-- Block-level predicates++isThematicBreak :: Text -> Bool+isThematicBreak l' = T.length l >= 3 && indentLevel l' < 4 &&+ (T.all (== '*') l ||+ T.all (== '-') l ||+ T.all (== '_') l)+ where+ l = T.filter (not . isSpace) l'++----------------------------------------------------------------------------+-- Other helpers++isSpace :: Char -> Bool+isSpace x = x == ' ' || x == '\t'++isSpaceN :: Char -> Bool+isSpaceN x = isSpace x || x == '\n' || x == '\r'++notNewline :: Char -> Bool+notNewline x = x /= '\n' && x /= '\r'++isBlank :: Text -> Bool+isBlank = T.all isSpace++isMarkupChar :: Char -> Bool+isMarkupChar = \case+ '*' -> True+ '~' -> True+ '_' -> True+ '`' -> True+ '^' -> True+ '[' -> True+ ']' -> True+ _ -> False++isAsciiPunctuation :: Char -> Bool+isAsciiPunctuation x =+ (x >= '!' && x <= '/') ||+ (x >= ':' && x <= '@') ||+ (x >= '[' && x <= '`') ||+ (x >= '{' && x <= '~')++isTransparentPunctuation :: Char -> Bool+isTransparentPunctuation = \case+ '!' -> True+ '"' -> True+ '(' -> True+ ')' -> True+ ',' -> True+ '-' -> True+ '.' -> True+ ':' -> True+ ';' -> True+ '?' -> True+ '{' -> True+ '}' -> True+ '–' -> True+ '—' -> True+ _ -> False++isTransparent :: Char -> Bool+isTransparent x = Char.isSpace x || isTransparentPunctuation x++nes :: a -> NonEmpty a+nes a = a :| []++assembleParagraph :: [Text] -> Text+assembleParagraph = go+ where+ go [] = ""+ go [x] = T.dropWhileEnd isSpace x+ go (x:xs) = x <> "\n" <> go xs++assembleCodeBlock :: Pos -> [Text] -> Text+assembleCodeBlock indent ls = T.unlines (stripIndent indent <$> ls)++indentLevel :: Text -> Int+indentLevel = T.foldl' f 0 . T.takeWhile isSpace+ where+ f n ch+ | ch == ' ' = n + 1+ | ch == '\t' = n + 4+ | otherwise = n++stripIndent :: Pos -> Text -> Text+stripIndent indent txt = T.drop m txt+ where+ m = snd $ T.foldl' f (0, 0) (T.takeWhile isSpace txt)+ f (!j, !n) ch+ | j >= i = (j, n)+ | ch == ' ' = (j + 1, n + 1)+ | ch == '\t' = (j + 4, n + 1)+ | otherwise = (j, n)+ i = unPos indent - 1++collapseWhiteSpace :: Text -> Text+collapseWhiteSpace =+ T.stripEnd . T.filter (/= '\0') . snd . T.mapAccumL f True+ where+ f seenSpace ch =+ case (seenSpace, g ch) of+ (False, False) -> (False, ch)+ (True, False) -> (False, ch)+ (False, True) -> (True, ' ')+ (True, True) -> (True, '\0')+ g ' ' = True+ g '\t' = True+ g '\n' = True+ g _ = False++inlineFrameDel :: InlineFrame -> Text+inlineFrameDel = \case+ EmphasisFrame -> "*"+ EmphasisFrame_ -> "_"+ StrongFrame -> "**"+ StrongFrame_ -> "__"+ StrikeoutFrame -> "~~"+ SubscriptFrame -> "~"+ SuperscriptFrame -> "^"++inlineStateDel :: InlineState -> Text+inlineStateDel = \case+ SingleFrame x -> inlineFrameDel x+ DoubleFrame x y -> inlineFrameDel x <> inlineFrameDel y++liftFrame :: InlineFrame -> NonEmpty Inline -> Inline+liftFrame = \case+ StrongFrame -> Strong+ EmphasisFrame -> Emphasis+ StrongFrame_ -> Strong+ EmphasisFrame_ -> Emphasis+ StrikeoutFrame -> Strikeout+ SubscriptFrame -> Subscript+ SuperscriptFrame -> Superscript++replaceEof :: String -> ParseError Char e -> ParseError Char e+replaceEof altLabel = \case+ TrivialError pos us es -> TrivialError pos (f <$> us) (E.map f es)+ FancyError pos xs -> FancyError pos xs+ where+ f EndOfInput = Label (NE.fromList altLabel)+ f x = x++mmarkErr :: MonadParsec MMarkErr s m => MMarkErr -> m a+mmarkErr = fancyFailure . E.singleton . ErrorCustom++toNesTokens :: Text -> NonEmpty Char+toNesTokens = NE.fromList . T.unpack++isEmailUri :: URI -> Maybe Text+isEmailUri uri =+ case URI.unRText <$> URI.uriPath uri of+ [x] ->+ if Email.isValid (TE.encodeUtf8 x) &&+ (isNothing (URI.uriScheme uri) ||+ URI.uriScheme uri == Just mailtoScheme)+ then Just x+ else Nothing+ _ -> Nothing++mailtoScheme :: URI.RText 'URI.Scheme+mailtoScheme = fromJust (URI.mkScheme "mailto")++splitYamlError :: FilePath -> String -> (Maybe SourcePos, String)+splitYamlError file str = maybe (Nothing, str) (first pure) (parseMaybe p str)+ where+ p :: Parsec Void String (SourcePos, String)+ p = do+ void (string "YAML parse exception at line ")+ l <- mkPos . (+ 2) <$> L.decimal+ void (string ", column ")+ c <- mkPos . (+ 1) <$> L.decimal+ void (string ":\n")+ r <- takeRest+ return (SourcePos file l c, r)
+ bench/memory/Main.hs view
@@ -0,0 +1,27 @@+module Main (main) where++import Weigh+import qualified Data.Text.IO as T+import qualified Text.MMark as MMark++main :: IO ()+main = mainWith $ do+ setColumns [Case, Allocated, GCs, Max]+ bparser "data/bench-yaml-block.md"+ bparser "data/bench-thematic-break.md"+ bparser "data/bench-heading.md"+ bparser "data/bench-fenced-code-block.md"+ bparser "data/bench-indented-code-block.md"+ bparser "data/bench-paragraph.md"+ bparser "data/comprehensive.md"++----------------------------------------------------------------------------+-- Helpers++bparser+ :: FilePath -- ^ File from which the input has been loaded+ -> Weigh ()+bparser path = action name (p <$> T.readFile path)+ where+ name = "with file: " ++ path+ p = MMark.parse path
+ bench/speed/Main.hs view
@@ -0,0 +1,27 @@+module Main (main) where++import Criterion.Main+import qualified Data.Text.IO as T+import qualified Text.MMark as MMark++main :: IO ()+main = defaultMain+ [ bparser "data/bench-yaml-block.md"+ , bparser "data/bench-thematic-break.md"+ , bparser "data/bench-heading.md"+ , bparser "data/bench-fenced-code-block.md"+ , bparser "data/bench-indented-code-block.md"+ , bparser "data/bench-paragraph.md"+ , bparser "data/comprehensive.md"+ ]++----------------------------------------------------------------------------+-- Helpers++bparser+ :: FilePath -- ^ File from which to load parser's input+ -> Benchmark+bparser path = env (T.readFile path) (bench name . nf p)+ where+ name = "with file: " ++ path+ p = MMark.parse path
+ data/bench-fenced-code-block.md view
@@ -0,0 +1,12 @@+```+Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat+massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi+posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent+egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut+ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et+magnis dis parturient montes, nascetur ridiculus mus. Integer interdum+malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet+sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.+Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae+massa luctus malesuada a vel diam.+```
+ data/bench-heading.md view
@@ -0,0 +1,6 @@+# Heading 1 blah blah blah+## Heading 2 blah blah blah+### Heading 3 blah blah blah+#### Heading 4 blah blah blah+##### Heading 5 blah blah blah+###### Heading 6 blah blah blah
+ data/bench-indented-code-block.md view
@@ -0,0 +1,10 @@+ Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat+ massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi+ posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent+ egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut+ ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et+ magnis dis parturient montes, nascetur ridiculus mus. Integer interdum+ malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet+ sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.+ Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae+ massa luctus malesuada a vel diam.
+ data/bench-paragraph.md view
@@ -0,0 +1,10 @@+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer varius mi+orci, rhoncus ornare nunc tincidunt nec. Aliquam cursus posuere ornare.+Quisque posuere euismod nunc, sed pellentesque metus hendrerit eu. Donec+scelerisque accumsan ante quis interdum. Nullam nec mauris dolor. Lorem+ipsum dolor sit amet, consectetur adipiscing elit. Phasellus id porttitor+nunc, sed laoreet eros. Maecenas ipsum ex, sagittis ut quam quis, vehicula+fringilla tortor. Vestibulum quis consequat mauris, sed porta risus.+Vestibulum nec ornare leo. Cras pharetra, ex sed dapibus pretium, diam+lectus accumsan enim, at malesuada tellus lorem et orci. Sed condimentum+varius ex in mollis.
+ data/bench-thematic-break.md view
@@ -0,0 +1,6 @@+***+---+__**+ - - -+ ** * ** * ** * **+- - - -
+ data/bench-yaml-block.md view
@@ -0,0 +1,7 @@+---+title: Free monad considered harmful+desc: Before you start writing your code using free monads read this, you may change your mind.+date:+ published: September 27, 2017+ updated: September 29, 2017+---
+ data/comprehensive.html view
@@ -0,0 +1,139 @@+<h1 id="lorem-ipsum">Lorem ipsum</h1>+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer varius mi+orci, rhoncus ornare nunc tincidunt nec. Aliquam cursus posuere ornare.+Quisque posuere euismod nunc, sed pellentesque metus hendrerit eu. Donec+scelerisque accumsan ante quis interdum. Nullam nec mauris dolor. Lorem+ipsum dolor sit amet, consectetur adipiscing elit. Phasellus id porttitor+nunc, sed laoreet eros. Maecenas ipsum ex, sagittis ut quam quis, vehicula+fringilla tortor. Vestibulum quis consequat mauris, sed porta risus.+Vestibulum nec ornare leo. Cras pharetra, ex sed dapibus pretium, diam+lectus accumsan enim, at malesuada tellus lorem et orci. Sed condimentum+varius ex in mollis.</p>+<p><a href="https://example.org/">https://example.org/</a></p>+<p>Ut in imperdiet neque. Etiam iaculis rhoncus nisl vel porta. Praesent velit+orci, laoreet suscipit bibendum eu, ornare et orci. Fusce feugiat, felis a+vehicula pulvinar, nulla purus dictum arcu, et varius urna purus et nibh.+Duis lobortis fringilla ligula, in aliquet sem maximus a. Suspendisse+potenti. Nullam consequat tellus a lectus vestibulum faucibus. Ut hendrerit+dolor ut libero efficitur accumsan. Mauris dapibus, leo non porttitor+lobortis, lectus ipsum tempor metus, quis iaculis arcu quam malesuada nulla.</p>+<h2 id="nullam-luctus">Nullam luctus</h2>+<p>Nullam <a href="http://example.org/luctus">luctus</a> placerat nisl in dapibus.+Phasellus id erat eros. Ut gravida risus sit amet massa tempor volutpat.+Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere+cubilia Curae; Quisque dictum sapien vel enim tempor, quis ornare justo+consequat. Suspendisse porttitor mollis consectetur. Curabitur sodales,+risus eu dapibus mattis, tellus dolor condimentum dolor, in ultricies nibh+augue vel nibh. Vivamus imperdiet, orci id posuere sollicitudin, diam purus+consequat eros, quis dictum mauris lacus ac lectus. Cras vitae pharetra+risus. Maecenas vehicula, leo vitae semper tristique, libero urna+consectetur massa, eget pharetra magna est nec massa. Vestibulum malesuada+lobortis lacinia.</p>+<p><img src="https://example.org/image.png" alt="My image"></p>+<p>Phasellus tincidunt metus quam, vel mollis turpis ultrices et. Phasellus+consequat diam eu turpis sollicitudin tempus. Fusce suscipit bibendum nisl,+quis rutrum eros volutpat in. Fusce tempor nisi eu ligula volutpat, eu+ultricies arcu blandit. Pellentesque habitant morbi tristique senectus et+netus et malesuada fames ac turpis egestas. Duis eleifend malesuada+venenatis. Morbi tincidunt quis diam ac aliquam.</p>+<h2 id="sed-euismod">Sed euismod</h2>+<p>Sed euismod nisi lorem, ac tempor nibh venenatis at. Integer porta nibh quis+mauris vehicula porta. Sed vel tellus nec lacus porttitor sollicitudin. Sed+facilisis nisl lorem, sed aliquet leo convallis sed. Curabitur vitae aliquet+diam, ac commodo ligula. Nulla aliquet odio at tellus auctor pellentesque.+In sagittis elementum tortor sed lobortis. Fusce nibh turpis, posuere eget+tristique eget, commodo quis leo.</p>+<hr>+<p>Etiam faucibus, ipsum id lobortis molestie, dolor lectus cursus purus, nec+volutpat massa odio vitae ligula. Nulla non consectetur ligula. In sem+felis, vehicula a convallis ut, pellentesque nec diam. Integer ullamcorper+rutrum nulla. Nam arcu dolor, placerat nec molestie et, eleifend sit amet+ante. Suspendisse laoreet orci sit amet vestibulum varius. In at leo eu+lorem tincidunt facilisis. Ut elementum elit ornare risus convallis, ut+viverra orci pretium. Vivamus mi orci, lacinia ac ligula a, condimentum+aliquet ligula.</p>+<h3 id="curabitur-ullamcorper">Curabitur ullamcorper</h3>+<p>Curabitur ullamcorper, lectus id porttitor vehicula, augue purus ornare+orci, ut consequat tellus mauris ac sem. Cras tincidunt sagittis mi, sit+amet viverra erat ultrices vulputate. Donec urna nulla, malesuada non cursus+et, posuere eu sapien. Fusce cursus mauris odio, id tincidunt felis+tincidunt sed. Duis vulputate lectus eu tellus pretium gravida. Nunc at eros+fringilla mi egestas imperdiet. In bibendum justo sapien, sed commodo tellus+auctor sit amet. Fusce at purus turpis. Aliquam a nibh at massa hendrerit+mollis a nec ipsum. Sed porta erat vitae justo sodales gravida nec sed+augue. Maecenas ultrices tristique hendrerit.</p>+<p>Curabitur venenatis vestibulum quam, a facilisis odio dignissim in.+Vestibulum ut turpis pharetra, aliquam metus a, dapibus massa. Mauris+vehicula sapien quis dolor facilisis, in placerat ipsum accumsan. Morbi+accumsan accumsan velit, vel auctor mauris suscipit non. Proin ex lacus,+dapibus et leo eu, lacinia viverra metus. Suspendisse potenti. Maecenas+finibus justo lectus, nec commodo velit cursus sit amet. In at lacus vel+augue porta volutpat eleifend ut orci.</p>+<h2 id="ut-dictum-quis">Ut dictum quis</h2>+<p>Ut dictum quis felis quis auctor. Phasellus lacinia tellus quis massa+commodo dignissim. Nam vitae felis non ex auctor tincidunt sit amet et+lorem. Nulla bibendum odio suscipit, tincidunt felis eget, mollis tortor.+Aliquam a feugiat augue, sed posuere massa. Donec pellentesque sodales+varius. Suspendisse quis mollis massa. Morbi imperdiet at felis in feugiat.+Praesent mauris urna, suscipit vitae condimentum venenatis, fermentum sit+amet purus. Cras non placerat nulla, vel lobortis ex. Nunc ut pulvinar+magna. Nulla vitae arcu turpis. Suspendisse elementum odio eros. Nullam+blandit molestie nibh eget pulvinar. Quisque placerat ante nec pulvinar+fringilla.</p>+<pre><code>Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat+massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi+posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent+egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut+ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et+magnis dis parturient montes, nascetur ridiculus mus. Integer interdum+malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet+sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.+Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae+massa luctus malesuada a vel diam.+</code></pre>+<p>Proin sit amet erat consequat, pellentesque augue at, efficitur libero. Nunc+pretium nulla eros, vitae euismod ex commodo sit amet. Pellentesque laoreet+ac enim id commodo. Integer eleifend nibh quis lorem pellentesque venenatis.+Nulla et eros sit amet diam pharetra porttitor. Sed ullamcorper rutrum+volutpat. Quisque accumsan posuere arcu, scelerisque dignissim ipsum. Donec+congue odio ultricies eros maximus, id maximus metus bibendum. Aenean ornare+metus non nisi varius venenatis. Vestibulum ornare id urna quis tempus. Cras+non arcu vitae magna feugiat sodales. Fusce eget posuere ligula. Donec+porttitor, odio at scelerisque tempus, nunc enim cursus dui, finibus+tincidunt justo lorem quis urna. Integer mauris quam, ultrices quis enim ut,+consectetur consequat sapien. In dapibus arcu eget ultrices vehicula.</p>+<h3 id="mauris-efficitur">Mauris efficitur</h3>+<p>Mauris efficitur mollis purus, nec porttitor ligula condimentum a. Proin+ultricies semper neque ac pulvinar. Nullam vestibulum leo justo, eget+vehicula elit lobortis vitae. Suspendisse commodo nunc et lorem molestie+lobortis. Pellentesque habitant morbi tristique senectus et netus et+malesuada fames ac turpis egestas. Fusce nec mi ultricies, sagittis neque+sed, tincidunt leo. Proin in lorem a libero maximus posuere. Pellentesque+habitant morbi tristique senectus et netus et malesuada fames ac turpis+egestas. Sed interdum eget ipsum id ullamcorper.</p>+<p>Aenean feugiat orci leo. Morbi fringilla, tortor id posuere mollis, lectus+est ullamcorper tellus, nec sagittis augue nibh nec est. Sed pulvinar orci a+justo eleifend dapibus. Phasellus aliquam enim in semper tincidunt. Donec+tempor tristique purus eu pretium. In ornare est at varius elementum. Cras+finibus nisl in nisi vestibulum, sed sollicitudin eros finibus.</p>+<p>Nullam sed nisi blandit, ultrices neque a, finibus ligula. Etiam a ante+sagittis nibh ornare commodo. Morbi id elit vehicula, gravida enim vitae,+suscipit orci. Nunc mattis enim sit amet sem dictum molestie. Nam semper+arcu a libero molestie, at accumsan turpis egestas. Quisque at pulvinar+enim. Cras porttitor, dolor et commodo accumsan, nibh sem dapibus velit, sit+amet maximus urna justo id eros. Praesent vitae aliquet lorem, in facilisis+nulla. Proin sit amet odio nisl. Phasellus sed felis velit. Maecenas pretium+posuere laoreet. Nullam dapibus ullamcorper sapien. Integer nec quam elit.+Fusce at dictum lacus. Nam risus dui, efficitur eget urna at, ultricies+sagittis ligula.</p>+<h2 id="aenean-auctor-nec">Aenean auctor nec</h2>+<p>Aenean auctor nec libero ut laoreet. Etiam vulputate lorem quis ex+dignissim, sit amet tincidunt ligula scelerisque. Nam venenatis blandit+ipsum quis maximus. Curabitur vitae dolor a diam viverra efficitur. Nulla+feugiat iaculis orci quis sodales. Aliquam consequat bibendum turpis, quis+scelerisque lorem fermentum feugiat. Proin pellentesque interdum odio ac+ullamcorper. Proin laoreet purus leo, non eleifend velit venenatis id.+Aliquam consectetur finibus turpis quis sollicitudin. Duis congue est nec+odio hendrerit scelerisque at at sapien. Nunc lacinia ac leo sit amet+sodales. Donec posuere fermentum massa, at tristique nulla aliquet nec.+Mauris dapibus quam non augue maximus suscipit.</p>
+ data/comprehensive.md view
@@ -0,0 +1,164 @@+# Lorem ipsum++Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer varius mi+orci, rhoncus ornare nunc tincidunt nec. Aliquam cursus posuere ornare.+Quisque posuere euismod nunc, sed pellentesque metus hendrerit eu. Donec+scelerisque accumsan ante quis interdum. Nullam nec mauris dolor. Lorem+ipsum dolor sit amet, consectetur adipiscing elit. Phasellus id porttitor+nunc, sed laoreet eros. Maecenas ipsum ex, sagittis ut quam quis, vehicula+fringilla tortor. Vestibulum quis consequat mauris, sed porta risus.+Vestibulum nec ornare leo. Cras pharetra, ex sed dapibus pretium, diam+lectus accumsan enim, at malesuada tellus lorem et orci. Sed condimentum+varius ex in mollis.++<https://example.org>++Ut in imperdiet neque. Etiam iaculis rhoncus nisl vel porta. Praesent velit+orci, laoreet suscipit bibendum eu, ornare et orci. Fusce feugiat, felis a+vehicula pulvinar, nulla purus dictum arcu, et varius urna purus et nibh.+Duis lobortis fringilla ligula, in aliquet sem maximus a. Suspendisse+potenti. Nullam consequat tellus a lectus vestibulum faucibus. Ut hendrerit+dolor ut libero efficitur accumsan. Mauris dapibus, leo non porttitor+lobortis, lectus ipsum tempor metus, quis iaculis arcu quam malesuada nulla.++## Nullam luctus++Nullam [luctus](http://example.org/luctus) placerat nisl in dapibus.+Phasellus id erat eros. Ut gravida risus sit amet massa tempor volutpat.+Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere+cubilia Curae; Quisque dictum sapien vel enim tempor, quis ornare justo+consequat. Suspendisse porttitor mollis consectetur. Curabitur sodales,+risus eu dapibus mattis, tellus dolor condimentum dolor, in ultricies nibh+augue vel nibh. Vivamus imperdiet, orci id posuere sollicitudin, diam purus+consequat eros, quis dictum mauris lacus ac lectus. Cras vitae pharetra+risus. Maecenas vehicula, leo vitae semper tristique, libero urna+consectetur massa, eget pharetra magna est nec massa. Vestibulum malesuada+lobortis lacinia.++++Phasellus tincidunt metus quam, vel mollis turpis ultrices et. Phasellus+consequat diam eu turpis sollicitudin tempus. Fusce suscipit bibendum nisl,+quis rutrum eros volutpat in. Fusce tempor nisi eu ligula volutpat, eu+ultricies arcu blandit. Pellentesque habitant morbi tristique senectus et+netus et malesuada fames ac turpis egestas. Duis eleifend malesuada+venenatis. Morbi tincidunt quis diam ac aliquam.++## Sed euismod++Sed euismod nisi lorem, ac tempor nibh venenatis at. Integer porta nibh quis+mauris vehicula porta. Sed vel tellus nec lacus porttitor sollicitudin. Sed+facilisis nisl lorem, sed aliquet leo convallis sed. Curabitur vitae aliquet+diam, ac commodo ligula. Nulla aliquet odio at tellus auctor pellentesque.+In sagittis elementum tortor sed lobortis. Fusce nibh turpis, posuere eget+tristique eget, commodo quis leo.++***++Etiam faucibus, ipsum id lobortis molestie, dolor lectus cursus purus, nec+volutpat massa odio vitae ligula. Nulla non consectetur ligula. In sem+felis, vehicula a convallis ut, pellentesque nec diam. Integer ullamcorper+rutrum nulla. Nam arcu dolor, placerat nec molestie et, eleifend sit amet+ante. Suspendisse laoreet orci sit amet vestibulum varius. In at leo eu+lorem tincidunt facilisis. Ut elementum elit ornare risus convallis, ut+viverra orci pretium. Vivamus mi orci, lacinia ac ligula a, condimentum+aliquet ligula.++### Curabitur ullamcorper++Curabitur ullamcorper, lectus id porttitor vehicula, augue purus ornare+orci, ut consequat tellus mauris ac sem. Cras tincidunt sagittis mi, sit+amet viverra erat ultrices vulputate. Donec urna nulla, malesuada non cursus+et, posuere eu sapien. Fusce cursus mauris odio, id tincidunt felis+tincidunt sed. Duis vulputate lectus eu tellus pretium gravida. Nunc at eros+fringilla mi egestas imperdiet. In bibendum justo sapien, sed commodo tellus+auctor sit amet. Fusce at purus turpis. Aliquam a nibh at massa hendrerit+mollis a nec ipsum. Sed porta erat vitae justo sodales gravida nec sed+augue. Maecenas ultrices tristique hendrerit.++Curabitur venenatis vestibulum quam, a facilisis odio dignissim in.+Vestibulum ut turpis pharetra, aliquam metus a, dapibus massa. Mauris+vehicula sapien quis dolor facilisis, in placerat ipsum accumsan. Morbi+accumsan accumsan velit, vel auctor mauris suscipit non. Proin ex lacus,+dapibus et leo eu, lacinia viverra metus. Suspendisse potenti. Maecenas+finibus justo lectus, nec commodo velit cursus sit amet. In at lacus vel+augue porta volutpat eleifend ut orci.++## Ut dictum quis++Ut dictum quis felis quis auctor. Phasellus lacinia tellus quis massa+commodo dignissim. Nam vitae felis non ex auctor tincidunt sit amet et+lorem. Nulla bibendum odio suscipit, tincidunt felis eget, mollis tortor.+Aliquam a feugiat augue, sed posuere massa. Donec pellentesque sodales+varius. Suspendisse quis mollis massa. Morbi imperdiet at felis in feugiat.+Praesent mauris urna, suscipit vitae condimentum venenatis, fermentum sit+amet purus. Cras non placerat nulla, vel lobortis ex. Nunc ut pulvinar+magna. Nulla vitae arcu turpis. Suspendisse elementum odio eros. Nullam+blandit molestie nibh eget pulvinar. Quisque placerat ante nec pulvinar+fringilla.++```+Donec in feugiat quam, eget vulputate metus. Sed at velit aliquam, consequat+massa vel, pharetra urna. Donec sed auctor odio. In pretium magna porta mi+posuere malesuada. Morbi sagittis finibus ligula sit amet posuere. Praesent+egestas massa id leo ultricies bibendum. Nullam fringilla commodo mattis. Ut+ut odio eget elit suscipit imperdiet. Orci varius natoque penatibus et+magnis dis parturient montes, nascetur ridiculus mus. Integer interdum+malesuada sem, vitae faucibus risus hendrerit eget. Aliquam vitae imperdiet+sapien, eget feugiat dui. Morbi faucibus nulla et dolor aliquam scelerisque.+Praesent auctor aliquet egestas. Vivamus et blandit velit. Sed ut dui vitae+massa luctus malesuada a vel diam.+```++Proin sit amet erat consequat, pellentesque augue at, efficitur libero. Nunc+pretium nulla eros, vitae euismod ex commodo sit amet. Pellentesque laoreet+ac enim id commodo. Integer eleifend nibh quis lorem pellentesque venenatis.+Nulla et eros sit amet diam pharetra porttitor. Sed ullamcorper rutrum+volutpat. Quisque accumsan posuere arcu, scelerisque dignissim ipsum. Donec+congue odio ultricies eros maximus, id maximus metus bibendum. Aenean ornare+metus non nisi varius venenatis. Vestibulum ornare id urna quis tempus. Cras+non arcu vitae magna feugiat sodales. Fusce eget posuere ligula. Donec+porttitor, odio at scelerisque tempus, nunc enim cursus dui, finibus+tincidunt justo lorem quis urna. Integer mauris quam, ultrices quis enim ut,+consectetur consequat sapien. In dapibus arcu eget ultrices vehicula.++### Mauris efficitur++Mauris efficitur mollis purus, nec porttitor ligula condimentum a. Proin+ultricies semper neque ac pulvinar. Nullam vestibulum leo justo, eget+vehicula elit lobortis vitae. Suspendisse commodo nunc et lorem molestie+lobortis. Pellentesque habitant morbi tristique senectus et netus et+malesuada fames ac turpis egestas. Fusce nec mi ultricies, sagittis neque+sed, tincidunt leo. Proin in lorem a libero maximus posuere. Pellentesque+habitant morbi tristique senectus et netus et malesuada fames ac turpis+egestas. Sed interdum eget ipsum id ullamcorper.++Aenean feugiat orci leo. Morbi fringilla, tortor id posuere mollis, lectus+est ullamcorper tellus, nec sagittis augue nibh nec est. Sed pulvinar orci a+justo eleifend dapibus. Phasellus aliquam enim in semper tincidunt. Donec+tempor tristique purus eu pretium. In ornare est at varius elementum. Cras+finibus nisl in nisi vestibulum, sed sollicitudin eros finibus.++Nullam sed nisi blandit, ultrices neque a, finibus ligula. Etiam a ante+sagittis nibh ornare commodo. Morbi id elit vehicula, gravida enim vitae,+suscipit orci. Nunc mattis enim sit amet sem dictum molestie. Nam semper+arcu a libero molestie, at accumsan turpis egestas. Quisque at pulvinar+enim. Cras porttitor, dolor et commodo accumsan, nibh sem dapibus velit, sit+amet maximus urna justo id eros. Praesent vitae aliquet lorem, in facilisis+nulla. Proin sit amet odio nisl. Phasellus sed felis velit. Maecenas pretium+posuere laoreet. Nullam dapibus ullamcorper sapien. Integer nec quam elit.+Fusce at dictum lacus. Nam risus dui, efficitur eget urna at, ultricies+sagittis ligula.++## Aenean auctor nec++Aenean auctor nec libero ut laoreet. Etiam vulputate lorem quis ex+dignissim, sit amet tincidunt ligula scelerisque. Nam venenatis blandit+ipsum quis maximus. Curabitur vitae dolor a diam viverra efficitur. Nulla+feugiat iaculis orci quis sodales. Aliquam consequat bibendum turpis, quis+scelerisque lorem fermentum feugiat. Proin pellentesque interdum odio ac+ullamcorper. Proin laoreet purus leo, non eleifend velit venenatis id.+Aliquam consectetur finibus turpis quis sollicitudin. Duis congue est nec+odio hendrerit scelerisque at at sapien. Nunc lacinia ac leo sit amet+sodales. Donec posuere fermentum massa, at tristique nulla aliquet nec.+Mauris dapibus quam non augue maximus suscipit.
+ mmark.cabal view
@@ -0,0 +1,112 @@+name: mmark+version: 0.0.1.0+cabal-version: >= 1.18+tested-with: GHC==7.10.3, GHC==8.0.2, GHC==8.2.1+license: BSD3+license-file: LICENSE.md+author: Mark Karpov <markkarpov92@gmail.com>+maintainer: Mark Karpov <markkarpov92@gmail.com>+homepage: https://github.com/mrkkrp/mmark+bug-reports: https://github.com/mrkkrp/mmark/issues+category: Text+synopsis: Strict markdown processor for writers+build-type: Simple+description: Strict markdown processor for writers.+extra-doc-files: CHANGELOG.md+ , README.md+data-files: data/*.md+ , data/*.html++source-repository head+ type: git+ location: https://github.com/mrkkrp/mmark.git++flag dev+ description: Turn on development settings.+ manual: True+ default: False++library+ build-depends: aeson >= 0.11 && < 1.3+ , base >= 4.8 && < 5.0+ , containers >= 0.5 && < 0.6+ , data-default-class+ , deepseq >= 1.3 && < 1.5+ , email-validate >= 2.2 && < 2.4+ , foldl >= 1.2 && < 1.4+ , lucid >= 2.6 && < 3.0+ , megaparsec >= 6.1 && < 7.0+ , modern-uri >= 0.1.1 && < 0.2+ , mtl >= 2.0 && < 3.0+ , parser-combinators >= 0.2 && < 1.0+ , text >= 0.2 && < 1.3+ , yaml >= 0.8.10 && < 0.9+ if !impl(ghc >= 8.0)+ build-depends: semigroups == 0.18.*+ if !impl(ghc >= 7.10)+ build-depends: void == 0.7.*+ if !impl(ghc >= 8.0)+ build-depends: semigroups == 0.18.*+ exposed-modules: Text.MMark+ , Text.MMark.Extension+ other-modules: Text.MMark.Internal+ , Text.MMark.Parser+ if flag(dev)+ ghc-options: -O0 -Wall -Werror+ else+ ghc-options: -O2 -Wall+ default-language: Haskell2010++test-suite tests+ main-is: Spec.hs+ hs-source-dirs: tests+ type: exitcode-stdio-1.0+ build-depends: QuickCheck >= 2.4 && < 3.0+ , aeson >= 0.11 && < 1.3+ , base >= 4.8 && < 5.0+ , foldl >= 1.2 && < 1.4+ , hspec >= 2.0 && < 3.0+ , hspec-megaparsec >= 1.0 && < 2.0+ , lucid >= 2.6 && < 3.0+ , megaparsec >= 6.1 && < 7.0+ , mmark+ , modern-uri >= 0.1.1 && < 0.2+ , text >= 0.2 && < 1.3+ if !impl(ghc >= 8.0)+ build-depends: semigroups == 0.18.*+ other-modules: Text.MMarkSpec+ , Text.MMark.ExtensionSpec+ , Text.MMark.TestUtils+ if flag(dev)+ ghc-options: -O0 -Wall -Werror+ else+ ghc-options: -O2 -Wall+ default-language: Haskell2010++benchmark bench-speed+ main-is: Main.hs+ hs-source-dirs: bench/speed+ type: exitcode-stdio-1.0+ build-depends: base >= 4.8 && < 5.0+ , criterion >= 0.6.2.1 && < 1.3+ , mmark+ , text >= 0.2 && < 1.3+ if flag(dev)+ ghc-options: -O2 -Wall -Werror+ else+ ghc-options: -O2 -Wall+ default-language: Haskell2010++benchmark bench-memory+ main-is: Main.hs+ hs-source-dirs: bench/memory+ type: exitcode-stdio-1.0+ build-depends: base >= 4.8 && < 5.0+ , mmark+ , text >= 0.2 && < 1.3+ , weigh >= 0.0.4+ if flag(dev)+ ghc-options: -O2 -Wall -Werror+ else+ ghc-options: -O2 -Wall+ default-language: Haskell2010
+ tests/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ tests/Text/MMark/ExtensionSpec.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Text.MMark.ExtensionSpec (spec) where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Text (Text)+import Test.Hspec+import Test.QuickCheck+import Text.MMark.Extension (Block (..), Inline (..))+import Text.MMark.TestUtils+import qualified Data.Text as T+import qualified Lucid as L+import qualified Text.MMark as MMark+import qualified Text.MMark.Extension as Ext+import qualified Text.URI as URI++spec :: Spec+spec = parallel $ do+ describe "blockTrans" $+ it "works" $ do+ doc <- mkDoc "# My heading"+ toText (MMark.useExtension h1_to_h2 doc)+ `shouldBe` "<h2 id=\"my-heading\">My heading</h2>\n"+ describe "blockRender" $+ it "works" $ do+ doc <- mkDoc "# My heading"+ toText (MMark.useExtension add_h1_content doc)+ `shouldBe` "<h1 data-content=\"My heading\" id=\"my-heading\">My heading</h1>\n"+ describe "inlineTrans" $+ it "works" $ do+ doc <- mkDoc "# My *heading*"+ toText (MMark.useExtension em_to_strong doc)+ `shouldBe` "<h1 id=\"my-heading\">My <strong>heading</strong></h1>\n"+ describe "inlineRender" $+ it "works" $ do+ doc <- mkDoc "# My *heading*"+ toText (MMark.useExtension (add_em_class "foo") doc)+ `shouldBe` "<h1 id=\"my-heading\">My <em class=\"foo\">heading</em></h1>\n"+ describe "asPlainText" $ do+ let f x = Ext.asPlainText (x:|[])+ context "with Plain" $+ it "works" $+ property $ \txt ->+ f (Plain txt) `shouldBe` txt+ context "with LineBreak" $+ it "works" $+ f LineBreak `shouldBe` "\n"+ context "with Emphasis" $+ it "works" $+ property $ \txt ->+ f (Emphasis $ Plain txt :| []) `shouldBe` txt+ context "with Strong" $+ it "works" $+ property $ \txt ->+ f (Strong $ Plain txt :| []) `shouldBe` txt+ context "with Strikeout" $+ it "works" $+ property $ \txt ->+ f (Strikeout $ Plain txt :| []) `shouldBe` txt+ context "with Subscript" $+ it "works" $+ property $ \txt ->+ f (Subscript $ Plain txt :| []) `shouldBe` txt+ context "with Superscript" $+ it "works" $+ property $ \txt ->+ f (Superscript $ Plain txt :| []) `shouldBe` txt+ context "with CodeSpan" $+ it "works" $+ property $ \txt ->+ f (CodeSpan txt) `shouldBe` txt+ context "with Link" $+ it "works" $+ property $ \txt uri ->+ f (Link (Plain txt :| []) uri Nothing) `shouldBe` txt+ context "with Image" $+ it "works" $+ property $ \txt uri ->+ f (Image (Plain txt :| []) uri Nothing) `shouldBe` txt+ describe "headerId" $+ it "works" $+ Ext.headerId (Plain "Something like that":| []) `shouldBe`+ "something-like-that"+ describe "headerFragment" $+ it "generates URIs with just that fragment" $+ property $ \fragment -> do+ let uri = Ext.headerFragment fragment+ frag <- URI.mkFragment fragment+ URI.uriScheme uri `shouldBe` Nothing+ URI.uriAuthority uri `shouldBe` Left False+ URI.uriPath uri `shouldBe` []+ URI.uriQuery uri `shouldBe` []+ URI.uriFragment uri `shouldBe` Just frag++----------------------------------------------------------------------------+-- Arbitrary instances++instance Arbitrary Text where+ arbitrary = T.pack <$> arbitrary++----------------------------------------------------------------------------+-- Testing extensions++-- | Convert H1 headings into H2 headings.++h1_to_h2 :: MMark.Extension+h1_to_h2 = Ext.blockTrans $ \case+ Heading1 inner -> Heading2 inner+ other -> other++-- | Add a data attribute calculated based on plain text contents of the+-- level 1 heading to test the 'Ext.getOis' thing and 'Ext.blockRender' in+-- general.++add_h1_content :: MMark.Extension+add_h1_content = Ext.blockRender $ \old block ->+ case block of+ Heading1 inner -> L.with (old (Heading1 inner))+ [ L.data_ "content" (Ext.asPlainText . Ext.getOis . fst $ inner) ]+ other -> old other++-- | Covert all 'Emphasis' to 'Strong'.++em_to_strong :: MMark.Extension+em_to_strong = Ext.inlineTrans $ \case+ Emphasis inner -> Strong inner+ other -> other++-- | Add given class to all 'Emphasis' things.++add_em_class :: Text -> MMark.Extension+add_em_class given = Ext.inlineRender $ \old inline ->+ case inline of+ Emphasis inner -> L.with (old (Emphasis inner)) [L.class_ given]+ other -> old other
+ tests/Text/MMark/TestUtils.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE OverloadedStrings #-}++module Text.MMark.TestUtils+ ( -- * Document creation and rendering+ mkDoc+ , toText+ -- * Parser expectations+ , (~~->)+ , (~->)+ , (=->)+ , (==->) )+where++import Control.Monad+import Data.Text (Text)+import Test.Hspec+import Text.MMark (MMark, MMarkErr)+import Text.Megaparsec+import qualified Data.List.NonEmpty as NE+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Lucid as L+import qualified Text.MMark as MMark++----------------------------------------------------------------------------+-- Document creation and rendering++-- | Create an 'MMark' document from given input reporting an expectation+-- failure if it cannot be parsed.++mkDoc :: Text -> IO MMark+mkDoc input =+ case MMark.parse "" input of+ Left errs -> do+ expectationFailure $+ "while parsing a document, parse error(s) occurred:\n" +++ MMark.parseErrorsPretty input errs+ undefined+ Right x -> return x++-- | Render an 'MMark' document to 'Text'.++toText :: MMark -> Text+toText = TL.toStrict . L.renderText . MMark.render++----------------------------------------------------------------------------+-- Parser expectations++-- | Create an expectation that parser should fail producing a certain+-- collection of 'ParseError's.++infix 2 ~~->++(~~->)+ :: Text+ -- ^ Input for parser+ -> [ParseError Char MMarkErr]+ -- ^ Expected collection of parse errors, in order+ -> Expectation+input ~~-> errs'' =+ case MMark.parse "" input of+ Left errs' -> unless (errs == errs') . expectationFailure $+ "the parser is expected to fail with:\n" +++ MMark.parseErrorsPretty input errs +++ "but it failed with:\n" +++ MMark.parseErrorsPretty input errs'+ Right x -> expectationFailure $+ "the parser is expected to fail, but it parsed: " ++ T.unpack (toText x)+ where+ errs = NE.fromList errs''++-- | The same as @('~~->')@, but expects only one parse error.++infix 2 ~->++(~->)+ :: Text+ -- ^ Input for parser+ -> ParseError Char MMarkErr+ -- ^ Expected parse error to compare with+ -> Expectation+input ~-> err = input ~~-> [err]++-- | Test parser and render by specifying input for parser and expected+-- output of render.++infix 2 =->++(=->)+ :: Text -- ^ Input for MMark parser+ -> Text -- ^ Output of render to match against+ -> Expectation+input =-> expected =+ case MMark.parse "" input of+ Left errs -> expectationFailure $+ "the parser is expected to succeed, but it failed with:\n" +++ MMark.parseErrorsPretty input errs+ Right factual -> toText factual `shouldBe` expected++-- | Just like @('=->')@, but also appends newline to given input and tries+-- with that as well.++infix ==->++(==->)+ :: Text -- ^ Input for MMark parser+ -> Text -- ^ Output of render to match against+ -> Expectation+input ==-> expected = do+ input =-> expected+ mappend input "\n" =-> expected
+ tests/Text/MMarkSpec.hs view
@@ -0,0 +1,1197 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Text.MMarkSpec (spec) where++import Data.Aeson+import Data.Char+import Data.List.NonEmpty (NonEmpty (..))+import Data.Monoid+import Data.Text (Text)+import Test.Hspec+import Test.Hspec.Megaparsec+import Text.MMark (MMarkErr (..))+import Text.MMark.Extension (Inline (..))+import Text.MMark.TestUtils+import Text.Megaparsec (ErrorFancy (..))+import qualified Control.Foldl as L+import qualified Data.List.NonEmpty as NE+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import qualified Text.MMark as MMark+import qualified Text.MMark.Extension as Ext++spec :: Spec+spec = parallel $ do+ describe "parse and render" $ do+ context "2.2 Tabs" $ do+ it "CM1" $+ "\tfoo\tbaz\t\tbim" ==->+ "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n"+ it "CM2" $+ " \tfoo\tbaz\t\tbim" ==->+ "<pre><code>foo\tbaz\t\tbim\n</code></pre>\n"+ it "CM3" $+ " a\ta\n ὐ\ta" ==->+ "<pre><code>a\ta\nὐ\ta\n</code></pre>\n"+ xit "CM4" $ -- FIXME pending lists+ " - foo\n\n\tbar" ==->+ "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n"+ xit "CM5" $ -- FIXME pending lists+ "- foo\n\n\t\tbar" ==->+ "<ul>\n<li>\n<p>foo</p>\n<pre><code> bar\n</code></pre>\n</li>\n</ul>\n"+ xit "CM6" $ -- FIXME pending blockquotes+ ">\t\tfoo" ==->+ "<blockquote>\n<pre><code> foo\n</code></pre>\n</blockquote>\n"+ xit "CM7" $ -- FIXME pending lists+ "-\t\tfoo" ==->+ "<ul>\n<li>\n<pre><code> foo\n</code></pre>\n</li>\n</ul>\n"+ it "CM8" $+ " foo\n\tbar" ==->+ "<pre><code>foo\nbar\n</code></pre>\n"+ xit "CM9" $ -- FIXME pending lists+ " - foo\n - bar\n\t - baz" ==->+ "<ul>\n<li>foo\n<ul>\n<li>bar\n<ul>\n<li>baz</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n"+ it "CM10" $+ "#\tFoo" ==-> "<h1 id=\"foo\">Foo</h1>\n"+ it "CM11" $+ "*\t*\t*\t" ==-> "<hr>\n"+ context "3.1 Precedence" $+ xit "CM12" $ -- FIXME pending lists+ "- `one\n- two`" ==->+ "<ul>\n<li>`one</li>\n<li>two`</li>\n</ul>\n"+ context "4.1 Thematic breaks" $ do+ it "CM13" $+ "***\n---\n___" ==-> "<hr>\n<hr>\n<hr>\n"+ it "CM14" $+ "+++" ==-> "<p>+++</p>\n"+ it "CM15" $+ "===" ==-> "<p>===</p>\n"+ it "CM16" $+ let s = "--\n**\n__\n"+ in s ~-> errFancy (posN 4 s) (nonFlanking "*")+ it "CM17" $+ " ***\n ***\n ***" ==-> "<hr>\n<hr>\n<hr>\n"+ it "CM18" $+ " ***" ==-> "<pre><code>***\n</code></pre>\n"+ it "CM19" $+ let s = "Foo\n ***\n"+ in s ~-> errFancy (posN 10 s) (nonFlanking "*")+ it "CM20" $+ "_____________________________________" ==->+ "<hr>\n"+ it "CM21" $+ " - - -" ==-> "<hr>\n"+ it "CM22" $+ " ** * ** * ** * **" ==-> "<hr>\n"+ it "CM23" $+ "- - - -" ==-> "<hr>\n"+ it "CM24" $+ "- - - - " ==-> "<hr>\n"+ it "CM25" $+ let s = "_ _ _ _ a\n\na------\n\n---a---\n"+ in s ~-> errFancy posI (nonFlanking "_")+ it "CM26" $+ " *\\-*" ==-> "<p><em>-</em></p>\n"+ xit "CM27" $ -- FIXME pending lists+ "- foo\n***\n- bar" ==->+ "<ul>\n<li>foo</li>\n</ul>\n<hr />\n<ul>\n<li>bar</li>\n</ul>\n"+ it "CM28" $+ let s = "Foo\n***\nbar"+ in s ~-> errFancy (posN 6 s) (nonFlanking "*")+ xit "CM29" $ -- FIXME pending setext headings+ "Foo\n---\nbar" ==->+ "<h2>Foo</h2>\n<p>bar</p>\n"+ xit "CM30" $ -- FIXME pending lists+ "* Foo\n* * *\n* Bar" ==->+ "<ul>\n<li>Foo</li>\n</ul>\n<hr />\n<ul>\n<li>Bar</li>\n</ul>\n"+ xit "CM31" $ -- FIXME pending lists+ "- Foo\n- * * *" ==->+ "<ul>\n<li>Foo</li>\n<li>\n<hr />\n</li>\n</ul>\n"+ context "4.2 ATX headings" $ do+ it "CM32" $+ "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo" ==->+ "<h1 id=\"foo\">foo</h1>\n<h2 id=\"foo\">foo</h2>\n<h3 id=\"foo\">foo</h3>\n<h4 id=\"foo\">foo</h4>\n<h5 id=\"foo\">foo</h5>\n<h6 id=\"foo\">foo</h6>\n"+ it "CM33" $+ let s = "####### foo"+ in s ~-> err (posN 6 s) (utok '#' <> elabel "white space")+ it "CM34" $+ let s = "#5 bolt\n\n#hashtag"+ in s ~~->+ [ err (posN 1 s) (utok '5' <> etok '#' <> elabel "white space")+ , err (posN 10 s) (utok 'h' <> etok '#' <> elabel "white space") ]+ it "CM35" $+ "\\## foo" ==-> "<p>## foo</p>\n"+ it "CM36" $+ "# foo *bar* \\*baz\\*" ==-> "<h1 id=\"foo-bar-*baz*\">foo <em>bar</em> *baz*</h1>\n"+ it "CM37" $+ "# foo " ==->+ "<h1 id=\"foo\">foo</h1>\n"+ it "CM38" $+ " ### foo\n ## foo\n # foo" ==->+ "<h3 id=\"foo\">foo</h3>\n<h2 id=\"foo\">foo</h2>\n<h1 id=\"foo\">foo</h1>\n"+ it "CM39" $+ " # foo" ==-> "<pre><code># foo\n</code></pre>\n"+ it "CM40" $+ "foo\n # bar" ==-> "<p>foo\n# bar</p>\n"+ it "CM41" $+ "## foo ##\n ### bar ###" ==->+ "<h2 id=\"foo\">foo</h2>\n<h3 id=\"bar\">bar</h3>\n"+ it "CM42" $+ "# foo ##################################\n##### foo ##" ==->+ "<h1 id=\"foo\">foo</h1>\n<h5 id=\"foo\">foo</h5>\n"+ it "CM43" $+ "### foo ### " ==-> "<h3 id=\"foo\">foo</h3>\n"+ it "CM44" $+ "### foo ### b" ==-> "<h3 id=\"foo-###-b\">foo ### b</h3>\n"+ it "CM45" $+ "# foo#" ==-> "<h1 id=\"foo#\">foo#</h1>\n"+ it "CM46" $+ "### foo \\###\n## foo #\\##\n# foo \\#" ==->+ "<h3 id=\"foo-###\">foo ###</h3>\n<h2 id=\"foo-###\">foo ###</h2>\n<h1 id=\"foo-#\">foo #</h1>\n"+ it "CM47" $+ "****\n## foo\n****" ==->+ "<hr>\n<h2 id=\"foo\">foo</h2>\n<hr>\n"+ it "CM48" $+ "Foo bar\n# baz\nBar foo" ==->+ "<p>Foo bar\n# baz\nBar foo</p>\n"+ it "CM49" $+ let s = "## \n#\n### ###"+ in s ~~->+ [ err (posN 3 s) (utok '\n' <> elabel "heading character" <> elabel "white space")+ , err (posN 5 s) (utok '\n' <> etok '#' <> elabel "white space") ]+ context "4.4 Indented code blocks" $ do+ it "CM76" $+ " a simple\n indented code block" ==->+ "<pre><code>a simple\n indented code block\n</code></pre>\n"+ xit "CM77" $ -- FIXME pending lists+ " - foo\n\n bar" ==->+ "<ul>\n<li>\n<p>foo</p>\n<p>bar</p>\n</li>\n</ul>\n"+ xit "CM78" $ -- FIXME pending lists+ "1. foo\n\n - bar" ==->+ "<ol>\n<li>\n<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n</li>\n</ol>\n"+ it "CM79" $+ " <a/>\n *hi*\n\n - one" ==->+ "<pre><code><a/>\n*hi*\n\n- one\n</code></pre>\n"+ it "CM80" $+ " chunk1\n\n chunk2\n \n \n \n chunk3" ==->+ "<pre><code>chunk1\n\nchunk2\n\n\n\nchunk3\n</code></pre>\n"+ it "CM81" $+ " chunk1\n \n chunk2" ==->+ "<pre><code>chunk1\n \n chunk2\n</code></pre>\n"+ it "CM82" $+ "Foo\n bar\n" ==->+ "<p>Foo\nbar</p>\n"+ it "CM83" $+ " foo\nbar" ==->+ "<pre><code>foo\n</code></pre>\n<p>bar</p>\n"+ xit "CM84" $ -- FIXME pending setext headings+ "# Heading\n foo\nHeading\n------\n foo\n----\n" ==->+ "<h1>Heading</h1>\n<pre><code>foo\n</code></pre>\n<h2>Heading</h2>\n<pre><code>foo\n</code></pre>\n<hr />\n"+ it "CM85" $+ " foo\n bar" ==->+ "<pre><code> foo\nbar\n</code></pre>\n"+ it "CM86" $+ "\n \n foo\n \n" ==->+ "<pre><code>foo\n</code></pre>\n"+ it "CM87" $+ " foo " ==->+ "<pre><code>foo \n</code></pre>\n"+ context "4.5 Fenced code blocks" $ do+ it "CM88" $+ "```\n<\n >\n```" ==->+ "<pre><code><\n >\n</code></pre>\n"+ it "CM89" $+ "~~~\n<\n >\n~~~" ==->+ "<pre><code><\n >\n</code></pre>\n"+ it "CM90" $+ "```\naaa\n~~~\n```" ==->+ "<pre><code>aaa\n~~~\n</code></pre>\n"+ it "CM91" $+ "~~~\naaa\n```\n~~~" ==->+ "<pre><code>aaa\n```\n</code></pre>\n"+ it "CM92" $+ "````\naaa\n```\n``````" ==->+ "<pre><code>aaa\n```\n</code></pre>\n"+ it "CM93" $+ "~~~~\naaa\n~~~\n~~~~" ==->+ "<pre><code>aaa\n~~~\n</code></pre>\n"+ it "CM94" $+ let s = "```"+ in s ~-> err (posN 3 s)+ (ueof <> etok '`' <> elabel "info string" <> elabel "newline")+ it "CM95" $+ let s = "`````\n\n```\naaa\n"+ in s ~-> err (posN 15 s)+ (ueof <> elabel "closing code fence" <> elabel "code block content")+ xit "CM96" $ -- FIXME pending blockquotes+ "> ```\n> aaa\n\nbbb" ==->+ "<blockquote>\n<pre><code>aaa\n</code></pre>\n</blockquote>\n<p>bbb</p>\n"+ it "CM97" $+ "```\n\n \n```" ==->+ "<pre><code>\n \n</code></pre>\n"+ it "CM98" $+ "```\n```" ==->+ "<pre><code></code></pre>\n"+ it "CM99" $+ " ```\n aaa\naaa\n```" ==->+ "<pre><code>aaa\naaa\n</code></pre>\n"+ it "CM100" $+ " ```\naaa\n aaa\naaa\n ```" ==->+ "<pre><code>aaa\naaa\naaa\n</code></pre>\n"+ it "CM101" $+ " ```\n aaa\n aaa\n aaa\n ```" ==->+ "<pre><code>aaa\n aaa\naaa\n</code></pre>\n"+ it "CM102" $+ " ```\n aaa\n ```" ==->+ "<pre><code>```\naaa\n```\n</code></pre>\n"+ it "CM103" $+ "```\naaa\n ```" ==->+ "<pre><code>aaa\n</code></pre>\n"+ it "CM104" $+ " ```\naaa\n ```" ==->+ "<pre><code>aaa\n</code></pre>\n"+ it "CM105" $+ let s = "```\naaa\n ```\n"+ in s ~-> err (posN 16 s)+ (ueof <> elabel "closing code fence" <> elabel "code block content")+ it "CM106" $+ "``` ```\naaa" ==->+ "<p><code></code>\naaa</p>\n"+ it "CM107" $+ let s = "~~~~~~\naaa\n~~~ ~~\n"+ in s ~-> err (posN 18 s)+ (ueof <> elabel "closing code fence" <> elabel "code block content")+ it "CM108" $+ "foo\n```\nbar\n```\nbaz" ==->+ "<p>foo\n<code>bar</code>\nbaz</p>\n"+ xit "CM109" $ -- FIXME pending setext headings+ "foo\n---\n~~~\nbar\n~~~\n# baz" ==->+ "<h2>foo</h2>\n<pre><code>bar\n</code></pre>\n<h1>baz</h1>\n"+ it "CM110" $+ "```ruby\ndef foo(x)\n return 3\nend\n```" ==->+ "<pre><code class=\"language-ruby\">def foo(x)\n return 3\nend\n</code></pre>\n"+ it "CM111" $+ "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~" ==->+ "<pre><code class=\"language-ruby\">def foo(x)\n return 3\nend\n</code></pre>\n"+ it "CM112" $+ "````;\n````" ==->+ "<pre><code class=\"language-;\"></code></pre>\n"+ it "CM113" $+ "``` aa ```\nfoo" ==->+ "<p><code>aa</code>\nfoo</p>\n"+ it "CM114" $+ "```\n``` aaa\n```" ==->+ "<pre><code>``` aaa\n</code></pre>\n"+ context "4.8 Paragraphs" $ do+ it "CM180" $+ "aaa\n\nbbb" ==->+ "<p>aaa</p>\n<p>bbb</p>\n"+ it "CM181" $+ "aaa\nbbb\n\nccc\nddd" ==->+ "<p>aaa\nbbb</p>\n<p>ccc\nddd</p>\n"+ it "CM182" $+ "aaa\n\n\nbbb" ==->+ "<p>aaa</p>\n<p>bbb</p>\n"+ it "CM183" $+ " aaa\n bbb" ==->+ "<p>aaa\nbbb</p>\n"+ it "CM184" $+ "aaa\n bbb\n ccc" ==->+ "<p>aaa\nbbb\nccc</p>\n"+ it "CM185" $+ " aaa\nbbb" ==-> "<p>aaa\nbbb</p>\n"+ it "CM186" $+ " aaa\nbbb" ==->+ "<pre><code>aaa\n</code></pre>\n<p>bbb</p>\n"+ it "CM187" $+ "aaa \nbbb " ==->+ "<p>aaa\nbbb</p>\n"+ context "4.9 Blank lines" $+ it "CM188" $+ " \n\naaa\n \n\n# aaa\n\n " ==->+ "<p>aaa</p>\n<h1 id=\"aaa\">aaa</h1>\n"+ context "6 Inlines" $+ it "CM286" $+ let s = "`hi`lo`\n"+ in s ~-> err (posN 7 s) (ueib <> etok '`' <> elabel "code span content")+ context "6.1 Blackslash escapes" $ do+ it "CM287" $+ "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n"+ ==-> "<p>!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~</p>\n"+ it "CM288" $+ "\\\t\\A\\a\\ \\3\\φ\\«" ==->+ "<p>\\\t\\A\\a\\ \\3\\φ\\«</p>\n"+ it "CM289" $+ "\\*not emphasized\\*\n\\<br/> not a tag\n\\[not a link\\](/foo)\n\\`not code\\`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo\\]: /url \"not a reference\"\n" ==->+ "<p>*not emphasized*\n<br/> not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url "not a reference"</p>\n"+ it "CM290" $+ let s = "\\\\*emphasis*"+ in s ~-> err (posN 2 s) (utok '*' <> eeib <> eric)+ xit "CM291" $+ "foo\\\nbar" ==->+ "<p>foo<br>\nbar</p>\n"+ it "CM292" $+ "`` \\[\\` ``" ==->+ "<p><code>\\[\\`</code></p>\n"+ it "CM293" $+ " \\[\\]" ==->+ "<pre><code>\\[\\]\n</code></pre>\n"+ it "CM294" $+ "~~~\n\\[\\]\n~~~" ==->+ "<pre><code>\\[\\]\n</code></pre>\n"+ it "CM295" $+ "<http://example.com?find=*>" ==->+ "<p><a href=\"http://example.com/?find=*\">http://example.com/?find=*</a></p>\n"+ xit "CM296" $ -- FIXME pending HTML inlines+ "<a href=\"/bar\\/)\">" ==->+ "<p><a href="/bar/)"></p>\n"+ it "CM297" $+ let s = "[foo](/bar\\* \"ti\\*tle\")"+ in s ~-> err (posN 10 s)+ (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)+ xit "CM298" $ -- FIXME pending reference links+ "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"" ==->+ "<p><a href=\"/bar*\" title=\"ti*tle\">foo</a></p>\n"+ it "CM299" $+ "``` foo\\+bar\nfoo\n```" ==->+ "<pre><code class=\"language-foo+bar\">foo\n</code></pre>\n"+ context "6.2 Entity and numeric character references" $+ xit "CM300" $ -- FIXME pending entity references+ " & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸"+ ==-> "<p> & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸</p>\n"+ context "6.3 Code spans" $ do+ it "CM312" $+ "`foo`" ==-> "<p><code>foo</code></p>\n"+ it "CM313" $+ "`` foo ` bar ``" ==->+ "<p><code>foo ` bar</code></p>\n"+ it "CM314" $+ "` `` `" ==-> "<p><code>``</code></p>\n"+ it "CM315" $+ "``\nfoo\n``" ==-> "<p><code>foo</code></p>\n"+ it "CM316" $+ "`foo bar\n baz`" ==-> "<p><code>foo bar baz</code></p>\n"+ it "CM317" $+ "`a b`" ==-> "<p><code>a b</code></p>\n"+ it "CM318" $+ "`foo `` bar`" ==-> "<p><code>foo `` bar</code></p>\n"+ it "CM319" $+ let s = "`foo\\`bar`\n"+ in s ~-> err (posN 10 s) (ueib <> etok '`' <> elabel "code span content")+ it "CM320" $+ let s = "*foo`*`\n"+ in s ~-> err (posN 7 s) (ueib <> etok '*' <> eic)+ it "CM321" $+ let s = "[not a `link](/foo`)\n"+ in s ~-> err (posN 20 s) (ueib <> etok ']' <> eic <> eric)+ it "CM322" $+ let s = "`<a href=\"`\">`\n"+ in s ~-> err (posN 14 s) (ueib <> etok '`' <> elabel "code span content")+ xit "CM323" $ -- FIXME pending HTML inlines+ "<a href=\"`\">`" ==-> "<p><a href=\"`\">`</p>\n"+ it "CM324" $+ let s = "`<http://foo.bar.`baz>`\n"+ in s ~-> err (posN 23 s) (ueib <> etok '`' <> elabel "code span content")+ it "CM325" $+ "<http://foo.bar.`baz>`" ==->+ "<p><http://foo.bar.<code>baz></code></p>\n"+ it "CM326" $+ let s = "```foo``\n"+ in s ~-> err (posN 8 s) (ueib <> etok '`' <> elabel "code span content")+ it "CM327" $+ let s = "`foo\n"+ in s ~-> err (posN 4 s) (ueib <> etok '`' <> elabel "code span content")+ it "CM328" $+ let s = "`foo``bar``\n"+ in s ~-> err (posN 11 s) (ueib <> etok '`' <> elabel "code span content")+ context "6.4 Emphasis and strong emphasis" $ do+ it "CM329" $+ "*foo bar*" ==-> "<p><em>foo bar</em></p>\n"+ it "CM330" $+ let s = "a * foo bar*\n"+ in s ~-> err (posN 2 s) (utok '*' <> eeib <> eric)+ it "CM331" $+ let s = "a*\"foo\"*\n"+ in s ~-> err (posN 1 s) (utok '*' <> eeib <> eric)+ it "CM332" $+ let s = "* a *\n"+ in s ~-> errFancy posI (nonFlanking "*")+ it "CM333" $+ let s = "foo*bar*\n"+ in s ~-> err (posN 3 s) (utok '*' <> eeib <> eric)+ it "CM334" $+ let s = "5*6*78\n"+ in s ~-> err (posN 1 s) (utok '*' <> eeib <> eric)+ it "CM335" $+ "_foo bar_" ==-> "<p><em>foo bar</em></p>\n"+ it "CM336" $+ let s = "_ foo bar_\n"+ in s ~-> errFancy posI (nonFlanking "_")+ it "CM337" $+ let s = "a_\"foo\"_\n"+ in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)+ it "CM338" $+ let s = "foo_bar_\n"+ in s ~-> err (posN 3 s) (utok '_' <> eeib <> eric)+ it "CM339" $+ let s = "5_6_78\n"+ in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)+ it "CM340" $+ let s = "пристаням_стремятся_\n"+ in s ~-> err (posN 9 s) (utok '_' <> eeib <> eric)+ it "CM341" $+ let s = "aa_\"bb\"_cc\n"+ in s ~-> err (posN 2 s) (utok '_' <> eeib <> eric)+ it "CM342" $+ let s = "foo-_(bar)_\n"+ in s ~-> err (posN 4 s) (utok '_' <> eeib <> eric)+ it "CM343" $+ let s = "_foo*\n"+ in s ~-> err (posN 4 s) (utok '*' <> etok '_' <> eric)+ it "CM344" $+ let s = "*foo bar *\n"+ in s ~-> errFancy (posN 9 s) (nonFlanking "*")+ it "CM345" $+ let s = "*foo bar\n*\n"+ in s ~-> errFancy (posN 9 s) (nonFlanking "*")+ it "CM346" $+ let s = "*(*foo)\n"+ in s ~-> errFancy posI (nonFlanking "*")+ it "CM347" $+ let s = "*(*foo*)*\n"+ in s ~-> errFancy posI (nonFlanking "*")+ it "CM348" $+ let s = "*foo*bar\n"+ in s ~-> errFancy (posN 4 s) (nonFlanking "*")+ it "CM349" $+ let s = "_foo bar _\n"+ in s ~-> errFancy (posN 9 s) (nonFlanking "_")+ it "CM350" $+ let s = "_(_foo)\n"+ in s ~-> errFancy posI (nonFlanking "_")+ it "CM351" $+ let s = "_(_foo_)_\n"+ in s ~-> errFancy posI (nonFlanking "_")+ it "CM352" $+ let s = "_foo_bar\n"+ in s ~-> errFancy (posN 4 s) (nonFlanking "_")+ it "CM353" $+ let s = "_пристаням_стремятся\n"+ in s ~-> errFancy (posN 10 s) (nonFlanking "_")+ it "CM354" $+ let s = "_foo_bar_baz_\n"+ in s ~-> errFancy (posN 4 s) (nonFlanking "_")+ it "CM355" $+ "_\\(bar\\)_.\n" ==-> "<p><em>(bar)</em>.</p>\n"+ it "CM356" $+ "**foo bar**\n" ==-> "<p><strong>foo bar</strong></p>\n"+ it "CM357" $+ let s = "** foo bar**\n"+ in s ~-> errFancy (posN 1 s) (nonFlanking "*")+ it "CM358" $+ let s = "a**\"foo\"**\n"+ in s ~-> err (posN 1 s) (utok '*' <> eeib <> eric)+ it "CM359" $+ let s = "foo**bar**\n"+ in s ~-> err (posN 3 s) (utok '*' <> eeib <> eric)+ it "CM360" $+ "__foo bar__" ==-> "<p><strong>foo bar</strong></p>\n"+ it "CM361" $+ let s = "__ foo bar__\n"+ in s ~-> errFancy (posN 1 s) (nonFlanking "_")+ it "CM362" $+ let s = "__\nfoo bar__\n"+ in s ~-> errFancy (posN 1 s) (nonFlanking "_")+ it "CM363" $+ let s = "a__\"foo\"__\n"+ in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)+ it "CM364" $+ let s = "foo__bar__\n"+ in s ~-> err (posN 3 s) (utok '_' <> eeib <> eric)+ it "CM365" $+ let s = "5__6__78\n"+ in s ~-> err (posN 1 s) (utok '_' <> eeib <> eric)+ it "CM366" $+ let s = "пристаням__стремятся__\n"+ in s ~-> err (posN 9 s) (utok '_' <> eeib <> eric)+ it "CM367" $+ "__foo, __bar__, baz__" ==->+ "<p><strong>foo, <strong>bar</strong>, baz</strong></p>\n"+ it "CM368" $+ "foo-__\\(bar\\)__" ==-> "<p>foo-<strong>(bar)</strong></p>\n"+ it "CM369" $+ let s = "**foo bar **\n"+ in s ~-> errFancy (posN 11 s) (nonFlanking "*")+ it "CM370" $+ let s = "**(**foo)\n"+ in s ~-> errFancy (posN 1 s) (nonFlanking "*")+ it "CM371" $+ let s = "*(**foo**)*\n"+ in s ~-> errFancy posI (nonFlanking "*")+ xit "CM372" $ -- FIXME doesn't pass with current approach+ "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**" ==->+ "<p><strong>Gomphocarpus (<em>Gomphocarpus physocarpus</em>, syn.\n<em>Asclepias physocarpa</em>)</strong></p>\n"+ it "CM373" $+ "**foo \"*bar*\" foo**" ==->+ "<p><strong>foo "<em>bar</em>" foo</strong></p>\n"+ it "CM374" $+ let s = "**foo**bar\n"+ in s ~-> errFancy (posN 5 s) (nonFlanking "**")+ it "CM375" $+ let s = "__foo bar __\n"+ in s ~-> errFancy (posN 11 s) (nonFlanking "_")+ it "CM376" $+ let s = "__(__foo)\n"+ in s ~-> errFancy (posN 1 s) (nonFlanking "_")+ it "CM377" $+ let s = "_(__foo__)_\n"+ in s ~-> errFancy posI (nonFlanking "_")+ it "CM378" $+ let s = "__foo__bar\n"+ in s ~-> errFancy (posN 5 s) (nonFlanking "__")+ it "CM379" $+ let s = "__пристаням__стремятся\n"+ in s ~-> errFancy (posN 11 s) (nonFlanking "__")+ it "CM380" $+ "__foo\\_\\_bar\\_\\_baz__" ==->+ "<p><strong>foo__bar__baz</strong></p>\n"+ it "CM381" $+ "__\\(bar\\)__." ==->+ "<p><strong>(bar)</strong>.</p>\n"+ it "CM382" $+ "*foo [bar](/url)*" ==->+ "<p><em>foo <a href=\"/url\">bar</a></em></p>\n"+ it "CM383" $+ "*foo\nbar*" ==->+ "<p><em>foo\nbar</em></p>\n"+ it "CM384" $+ "_foo __bar__ baz_" ==->+ "<p><em>foo <strong>bar</strong> baz</em></p>\n"+ it "CM385" $+ "_foo _bar_ baz_" ==->+ "<p><em>foo <em>bar</em> baz</em></p>\n"+ it "CM386" $+ let s = "__foo_ bar_"+ in s ~-> err (posN 5 s) (utoks "_ " <> etoks "__" <> eric)+ it "CM387" $+ "*foo *bar**" ==->+ "<p><em>foo <em>bar</em></em></p>\n"+ it "CM388" $+ "*foo **bar** baz*" ==->+ "<p><em>foo <strong>bar</strong> baz</em></p>\n"+ it "CM389" $+ let s = "*foo**bar**baz*\n"+ in s ~-> err (posN 5 s) (utok '*' <> eeib)+ it "CM390" $+ "***foo** bar*\n" ==-> "<p><em><strong>foo</strong> bar</em></p>\n"+ it "CM391" $+ "*foo **bar***\n" ==-> "<p><em>foo <strong>bar</strong></em></p>\n"+ it "CM392" $+ let s = "*foo**bar***\n"+ in s ~-> err (posN 5 s) (utok '*' <> elabel "end of inline block")+ it "CM393" $+ "*foo **bar *baz* bim** bop*\n" ==->+ "<p><em>foo <strong>bar <em>baz</em> bim</strong> bop</em></p>\n"+ it "CM394" $+ "*foo [*bar*](/url)*\n" ==->+ "<p><em>foo <a href=\"/url\"><em>bar</em></a></em></p>\n"+ it "CM395" $+ let s = "** is not an empty emphasis\n"+ in s ~-> errFancy (posN 1 s) (nonFlanking "*")+ it "CM396" $+ let s = "**** is not an empty strong emphasis\n"+ in s ~-> errFancy (posN 3 s) (nonFlanking "*")+ it "CM397" $+ "**foo [bar](/url)**" ==->+ "<p><strong>foo <a href=\"/url\">bar</a></strong></p>\n"+ it "CM398" $+ "**foo\nbar**" ==->+ "<p><strong>foo\nbar</strong></p>\n"+ it "CM399" $+ "__foo _bar_ baz__" ==->+ "<p><strong>foo <em>bar</em> baz</strong></p>\n"+ it "CM400" $+ "__foo __bar__ baz__" ==->+ "<p><strong>foo <strong>bar</strong> baz</strong></p>\n"+ it "CM401" $+ "____foo__ bar__" ==->+ "<p><strong><strong>foo</strong> bar</strong></p>\n"+ it "CM402" $+ "**foo **bar****" ==->+ "<p><strong>foo <strong>bar</strong></strong></p>\n"+ it "CM403" $+ "**foo *bar* baz**" ==->+ "<p><strong>foo <em>bar</em> baz</strong></p>\n"+ it "CM404" $+ let s = "**foo*bar*baz**\n"+ in s ~-> err (posN 5 s) (utoks "*b" <> etoks "**" <> eric)+ it "CM405" $+ "***foo* bar**" ==->+ "<p><strong><em>foo</em> bar</strong></p>\n"+ it "CM406" $+ "**foo *bar***" ==->+ "<p><strong>foo <em>bar</em></strong></p>\n"+ it "CM407" $+ "**foo *bar **baz**\nbim* bop**" ==->+ "<p><strong>foo <em>bar <strong>baz</strong>\nbim</em> bop</strong></p>\n"+ it "CM408" $+ "**foo [*bar*](/url)**" ==->+ "<p><strong>foo <a href=\"/url\"><em>bar</em></a></strong></p>\n"+ it "CM409" $+ let s = "__ is not an empty emphasis\n"+ in s ~-> errFancy (posN 1 s) (nonFlanking "_")+ it "CM410" $+ let s = "____ is not an empty strong emphasis\n"+ in s ~-> errFancy (posN 3 s) (nonFlanking "_")+ it "CM411" $+ let s = "foo ***\n"+ in s ~-> errFancy (posN 6 s) (nonFlanking "*")+ it "CM412" $+ "foo *\\**" ==-> "<p>foo <em>*</em></p>\n"+ it "CM413" $+ "foo *\\_*\n" ==-> "<p>foo <em>_</em></p>\n"+ it "CM414" $+ let s = "foo *****\n"+ in s ~-> errFancy (posN 8 s) (nonFlanking "*")+ it "CM415" $+ "foo **\\***" ==-> "<p>foo <strong>*</strong></p>\n"+ it "CM416" $+ "foo **\\_**\n" ==-> "<p>foo <strong>_</strong></p>\n"+ it "CM417" $+ let s = "**foo*\n"+ in s ~-> err (posN 5 s) (utok '*' <> etoks "**" <> eric)+ it "CM418" $+ let s = "*foo**\n"+ in s ~-> err (posN 5 s) (utok '*' <> eeib)+ it "CM419" $+ let s = "***foo**\n"+ in s ~-> err (posN 8 s) (ueib <> etok '*' <> eic)+ it "CM420" $+ let s = "****foo*\n"+ in s ~-> err (posN 7 s) (utok '*' <> etoks "**" <> eric)+ it "CM421" $+ let s = "**foo***\n"+ in s ~-> err (posN 7 s) (utok '*' <> eeib)+ it "CM422" $+ let s = "*foo****\n"+ in s ~-> err (posN 5 s) (utok '*' <> eeib)+ it "CM423" $+ let s = "foo ___\n"+ in s ~-> errFancy (posN 6 s) (nonFlanking "_")+ it "CM424" $+ "foo _\\__" ==-> "<p>foo <em>_</em></p>\n"+ it "CM425" $+ "foo _\\*_" ==-> "<p>foo <em>*</em></p>\n"+ it "CM426" $+ let s = "foo _____\n"+ in s ~-> errFancy (posN 8 s) (nonFlanking "_")+ it "CM427" $+ "foo __\\___" ==-> "<p>foo <strong>_</strong></p>\n"+ it "CM428" $+ "foo __\\*__" ==-> "<p>foo <strong>*</strong></p>\n"+ it "CM429" $+ let s = "__foo_\n"+ in s ~-> err (posN 5 s) (utok '_' <> etoks "__" <> eric)+ it "CM430" $+ let s = "_foo__\n"+ in s ~-> err (posN 5 s) (utok '_' <> eeib)+ it "CM431" $+ let s = "___foo__\n"+ in s ~-> err (posN 8 s) (ueib <> etok '_' <> eic)+ it "CM432" $+ let s = "____foo_\n"+ in s ~-> err (posN 7 s) (utok '_' <> etoks "__" <> eric)+ it "CM433" $+ let s = "__foo___\n"+ in s ~-> err (posN 7 s) (utok '_' <> eeib)+ it "CM434" $+ let s = "_foo____\n"+ in s ~-> err (posN 5 s) (utok '_' <> eeib)+ it "CM435" $+ "**foo**" ==-> "<p><strong>foo</strong></p>\n"+ it "CM436" $+ "*_foo_*" ==-> "<p><em><em>foo</em></em></p>\n"+ it "CM437" $+ "__foo__" ==-> "<p><strong>foo</strong></p>\n"+ it "CM438" $+ "_*foo*_" ==-> "<p><em><em>foo</em></em></p>\n"+ it "CM439" $+ "****foo****" ==-> "<p><strong><strong>foo</strong></strong></p>\n"+ it "CM440" $+ "____foo____" ==-> "<p><strong><strong>foo</strong></strong></p>\n"+ it "CM441" $+ "******foo******" ==->+ "<p><strong><strong><strong>foo</strong></strong></strong></p>\n"+ it "CM442" $+ "***foo***" ==-> "<p><em><strong>foo</strong></em></p>\n"+ it "CM443" $+ "_____foo_____" ==->+ "<p><strong><strong><em>foo</em></strong></strong></p>\n"+ it "CM444" $+ let s = "*foo _bar* baz_\n"+ in s ~-> err (posN 9 s) (utok '*' <> etok '_' <> eric)+ it "CM445" $+ let s = "*foo __bar *baz bim__ bam*\n"+ in s ~-> err (posN 19 s) (utok '_' <> etok '*' <> eric)+ it "CM446" $+ let s = "**foo **bar baz**\n"+ in s ~-> err (posN 17 s) (ueib <> etoks "**" <> eic)+ it "CM447" $+ let s = "*foo *bar baz*\n"+ in s ~-> err (posN 14 s) (ueib <> etok '*' <> eic)+ it "CM448" $+ let s = "*[bar*](/url)\n"+ in s ~-> err (posN 5 s) (utok '*' <> etok ']' <> eric)+ it "CM449" $+ let s = "_foo [bar_](/url)\n"+ in s ~-> err (posN 9 s) (utok '_' <> etok ']' <> eric)+ xit "CM450" $ -- FIXME pending HTML inlines+ "*<img src=\"foo\" title=\"*\"/>" ==->+ "<p>*<img src=\"foo\" title=\"*\"/></p>\n"+ xit "CM451" $ -- FIXME pending HTML inlines+ "**<a href=\"**\">" ==-> "<p>**<a href=\"**\"></p>\n"+ xit "CM452" $+ "__<a href=\"__\">\n" ==-> "<p>__<a href=\"__\"></p>\n"+ it "CM453" $+ "*a `*`*" ==-> "<p><em>a <code>*</code></em></p>\n"+ it "CM454" $+ "_a `_`_" ==-> "<p><em>a <code>_</code></em></p>\n"+ it "CM455" $+ let s = "**a<http://foo.bar/?q=**>"+ in s ~-> err (posN 25 s) (ueib <> etoks "**" <> eic)+ it "CM456" $+ let s = "__a<http://foo.bar/?q=__>"+ in s ~-> err (posN 26 s) (ueib <> etoks "__" <> eic)+ context "6.5 Links" $ do+ it "CM457" $+ "[link](/uri \"title\")" ==->+ "<p><a href=\"/uri\" title=\"title\">link</a></p>\n"+ it "CM458" $+ "[link](/uri)" ==->+ "<p><a href=\"/uri\">link</a></p>\n"+ it "CM459" $+ "[link]()" ==->+ "<p><a href>link</a></p>\n"+ it "CM460" $+ "[link](<>)" ==->+ "<p><a href>link</a></p>\n"+ it "CM461" $+ let s = "[link](/my uri)\n"+ in s ~-> err (posN 11 s)+ (utok 'u' <> etok '"' <> etok '\'' <> etok '(' <> elabel "white space")+ it "CM462" $+ let s = "[link](</my uri>)\n"+ in s ~-> err (posN 11 s)+ (utok ' ' <> etok '#' <> etok '/' <> etok '>' <> etok '?' <> eppi)+ it "CM463" $+ let s = "[link](foo\nbar)\n"+ in s ~-> err (posN 11 s)+ (utok 'b' <> etok '"' <> etok '\'' <> etok '(' <> elabel "white space")+ it "CM464" $+ let s = "[link](<foo\nbar>)\n"+ in s ~-> err (posN 11 s)+ (utok '\n' <> etok '#' <> etok '/' <> etok '>' <> etok '?' <> eppi)+ it "CM465" $+ let s = "[link](\\(foo\\))"+ in s ~-> err (posN 7 s) (utok '\\' <> etoks "//" <> etok '#' <>+ etok '/' <> etok '<' <> etok '?' <> elabel "ASCII alpha character" <>+ euri <> elabel "path piece")+ it "CM466" $+ "[link](foo(and(bar)))\n" ==->+ "<p><a href=\"foo(and(bar\">link</a>))</p>\n"+ it "CM467" $+ let s = "[link](foo\\(and\\(bar\\))"+ in s ~-> err (posN 10 s) (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)+ it "CM468" $+ "[link](<foo(and(bar)>)" ==->+ "<p><a href=\"foo(and(bar)\">link</a></p>\n"+ it "CM469" $+ let s = "[link](foo\\)\\:)"+ in s ~-> err (posN 10 s) (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)+ it "CM470" $+ "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n"+ ==-> "<p><a href=\"#fragment\">link</a></p>\n<p><a href=\"http://example.com/#fragment\">link</a></p>\n<p><a href=\"http://example.com/?foo=3#frag\">link</a></p>\n"+ it "CM471" $+ let s = "[link](foo\\bar)"+ in s ~-> err (posN 10 s) (utok '\\' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)+ xit "CM472" $ -- FIXME pending entity references+ "[link](foo%20bä)"+ ==-> "<p><a href=\"foo%20b&auml;\">link</a></p>\n"+ it "CM473" $+ let s = "[link](\"title\")"+ in s ~-> err (posN 7 s)+ (utok '"' <> etoks "//" <> etok '#' <> etok '/' <> etok '<' <>+ etok '?' <> elabel "ASCII alpha character" <> euri <> elabel "path piece")+ it "CM474" $+ "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))" ==->+ "<p><a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a>\n<a href=\"/url\" title=\"title\">link</a></p>\n"+ xit "CM475" $ -- FIXME pending entity references+ "[link](/url \"title \\\""\")\n" ==->+ "<p><a href=\"/url\" title=\"title ""\">link</a></p>\n"+ it "CM476" $+ let s = "[link](/url \"title\")"+ in s ~-> err (posN 11 s)+ (utok ' ' <> etok '#' <> etok '/' <> etok '?' <> euri <> eppi)+ it "CM477" $+ let s = "[link](/url \"title \"and\" title\")\n"+ in s ~-> err (posN 20 s) (utok 'a' <> etok ')' <> elabel "white space")+ it "CM478" $+ "[link](/url 'title \"and\" title')" ==->+ "<p><a href=\"/url\" title=\"title "and" title\">link</a></p>\n"+ it "CM479" $+ "[link]( /uri\n \"title\" )" ==->+ "<p><a href=\"/uri\" title=\"title\">link</a></p>\n"+ it "CM480" $+ let s = "[link] (/uri)\n"+ in s ~-> err (posN 6 s) (utok ' ' <> etok '(')+ it "CM481" $+ let s = "[link [foo [bar]]](/uri)\n"+ in s ~-> err (posN 6 s) (utok '[' <> etok ']' <> eic <> eric)+ it "CM482" $+ let s = "[link] bar](/uri)\n"+ in s ~-> err (posN 6 s) (utok ' ' <> etok '(')+ it "CM483" $+ let s = "[link [bar](/uri)\n"+ in s ~-> err (posN 6 s) (utok '[' <> etok ']' <> eic <> eric)+ it "CM484" $+ "[link \\[bar](/uri)\n" ==->+ "<p><a href=\"/uri\">link [bar</a></p>\n"+ it "CM485" $+ "[link *foo **bar** `#`*](/uri)" ==->+ "<p><a href=\"/uri\">link <em>foo <strong>bar</strong> <code>#</code></em></a></p>\n"+ it "CM486" $+ "[](/uri)" ==->+ "<p><a href=\"/uri\"><img src=\"moon.jpg\" alt=\"moon\"></a></p>\n"+ it "CM487" $+ let s = "[foo [bar](/uri)](/uri)\n"+ in s ~-> err (posN 5 s) (utok '[' <> etok ']' <> eic <> eric)+ it "CM488" $+ let s = "[foo *[bar [baz](/uri)](/uri)*](/uri)\n"+ in s ~-> err (posN 11 s) (utok '[' <> etok ']' <> eic <> eric)+ it "CM489" $+ let s = "](uri2)](uri3)"+ in s ~-> err (posN 3 s) (utoks "[foo" <> eeib <> eic)+ it "CM490" $+ let s = "*[foo*](/uri)\n"+ in s ~-> err (posN 5 s) (utok '*' <> etok ']' <> eric)+ it "CM491" $+ let s = "[foo *bar](baz*)\n"+ in s ~-> err (posN 9 s) (utok ']' <> etok '*' <> eic <> eric)+ it "CM492" $+ let s = "*foo [bar* baz]\n"+ in s ~-> err (posN 9 s) (utok '*' <> etok ']' <> eric)+ xit "CM493" $ -- FIXME pending inline HTML+ let s = "[foo <bar attr=\"](baz)\">"+ in s ~-> err (posN 5 s) (utok '<' <> etok ']')+ it "CM494" $+ let s = "[foo`](/uri)`\n"+ in s ~-> err (posN 13 s) (ueib <> etok ']' <> eic)+ it "CM495" $+ "[foo<http://example.com/?search=](uri)>" ==->+ "<p><a href=\"uri\">foo<http://example.com/?search=</a>></p>\n"+ context "6.6 Images" $ do+ it "CM541" $+ "" ==->+ "<p><img src=\"/url\" title=\"title\" alt=\"foo\"></p>\n"+ it "CM542" $+ "" ==->+ "<p><img src=\"train.jpg\" title=\"train & tracks\" alt=\"foo bar\"></p>\n"+ it "CM543" $+ let s = "](/url2)\n"+ in s ~-> err (posN 6 s) (utok '!' <> etok ']')+ it "CM544" $+ "](/url2)" ==->+ "<p><img src=\"/url2\" alt=\"foo bar\"></p>\n"+ it "CM545" pending+ it "CM546" pending+ it "CM547" $+ "" ==->+ "<p><img src=\"train.jpg\" alt=\"foo\"></p>\n"+ it "CM548" $+ "My " ==->+ "<p>My <img src=\"/path/to/train.jpg\" title=\"title\" alt=\"foo bar\"></p>\n"+ it "CM549" $+ "" ==->+ "<p><img src=\"url\" alt=\"foo\"></p>\n"+ it "CM550" $+ "" ==-> "<p><img src=\"/url\" alt></p>\n"+ it "CM551-CM562" pending -- pending reference-style stuff+ context "6.7 Autolinks" $ do+ it "CM563" $+ "<http://foo.bar.baz>" ==->+ "<p><a href=\"http://foo.bar.baz/\">http://foo.bar.baz/</a></p>\n"+ it "CM564" $+ "<http://foo.bar.baz/test?q=hello&id=22&boolean>" ==->+ "<p><a href=\"http://foo.bar.baz/test?q=hello&id=22&boolean\">http://foo.bar.baz/test?q=hello&id=22&boolean</a></p>\n"+ it "CM565" $+ "<irc://foo.bar:2233/baz>" ==->+ "<p><a href=\"irc://foo.bar:2233/baz\">irc://foo.bar:2233/baz</a></p>\n"+ it "CM566" $+ "<MAILTO:FOO@BAR.BAZ>" ==->+ "<p><a href=\"mailto:FOO@BAR.BAZ\">FOO@BAR.BAZ</a></p>\n"+ it "CM567" $+ "<a+b+c:d>" ==->+ "<p><a href=\"a+b+c:d\">a+b+c:d</a></p>\n"+ it "CM568" $+ "<made-up-scheme://foo,bar>" ==->+ "<p><a href=\"made-up-scheme://foo/,bar\">made-up-scheme://foo/,bar</a></p>\n"+ it "CM569" $+ "<http://../>" ==->+ "<p><http://../></p>\n"+ it "CM570" $+ "<localhost:5001/foo>" ==->+ "<p><a href=\"localhost:5001/foo\">localhost:5001/foo</a></p>\n"+ it "CM571" $+ "<http://foo.bar/baz bim>\n" ==->+ "<p><http://foo.bar/baz bim></p>\n"+ it "CM572" $+ "<http://example.com/\\[\\>" ==->+ "<p><http://example.com/[></p>\n"+ it "CM573" $+ "<foo@bar.example.com>" ==->+ "<p><a href=\"mailto:foo@bar.example.com\">foo@bar.example.com</a></p>\n"+ it "CM574" $+ "<foo+special@Bar.baz-bar0.com>" ==->+ "<p><a href=\"mailto:foo+special@Bar.baz-bar0.com\">foo+special@Bar.baz-bar0.com</a></p>\n"+ it "CM575" $+ "<foo\\+@bar.example.com>" ==->+ "<p><foo+@bar.example.com></p>\n"+ it "CM576" $+ "<>" ==->+ "<p><></p>\n"+ it "CM577" $+ "< http://foo.bar >" ==->+ "<p>< http://foo.bar ></p>\n"+ it "CM578" $+ "<m:abc>" ==->+ "<p><a href=\"m:abc\">m:abc</a></p>\n"+ it "CM579" $+ "<foo.bar.baz>" ==->+ "<p><a href=\"foo.bar.baz\">foo.bar.baz</a></p>\n"+ it "CM580" $+ "http://example.com" ==->+ "<p>http://example.com</p>\n"+ it "CM581" $+ "foo@bar.example.com" ==->+ "<p>foo@bar.example.com</p>\n"+ context "6.9 Hard line breaks" $ do+ -- NOTE We currently do not support hard line breaks represented in+ -- markup as space before newline.+ xit "CM603" $+ "foo \nbaz" ==-> "<p>foo<br>\nbaz</p>\n"+ it "CM604" $+ "foo\\\nbaz\n" ==-> "<p>foo<br>\nbaz</p>\n"+ xit "CM605" $+ "foo \nbaz" ==-> "<p>foo<br>\nbaz</p>\n"+ xit "CM606" $+ "foo \n bar" ==-> "<p>foo<br>\nbar</p>\n"+ it "CM607" $+ "foo\\\n bar" ==-> "<p>foo<br>\nbar</p>\n"+ xit "CM608" $+ "*foo \nbar*" ==-> "<p><em>foo<br>\nbar</em></p>\n"+ it "CM609" $+ "*foo\\\nbar*" ==-> "<p><em>foo<br>\nbar</em></p>\n"+ it "CM610" $+ "`code \nspan`" ==-> "<p><code>code span</code></p>\n"+ it "CM611" $+ "`code\\\nspan`" ==-> "<p><code>code\\ span</code></p>\n"+ xit "CM612" $+ "<a href=\"foo \nbar\">" ==-> "<p><a href=\"foo \nbar\"></p>\n"+ xit "CM613" $ -- FIXME pending HTML inlines+ "<a href=\"foo\\\nbar\">" ==-> "<p><a href=\"foo\\\nbar\"></p>\n"+ it "CM614" $+ "foo\\" ==-> "<p>foo\\</p>\n"+ xit "CM615" $+ "foo " ==-> "<p>foo</p>\n"+ it "CM616" $+ "### foo\\" ==-> "<h3 id=\"foo\\\">foo\\</h3>\n"+ it "CM617" $+ "### foo " ==-> "<h3 id=\"foo\">foo</h3>\n"+ context "6.10 Soft line breaks" $ do+ it "CM618" $+ "foo\nbaz" ==-> "<p>foo\nbaz</p>\n"+ it "CM619" $+ "foo \n baz" ==-> "<p>foo\nbaz</p>\n"+ context "6.11 Textual content" $ do+ it "CM620" $+ "hello $.;'there" ==-> "<p>hello $.;'there</p>\n"+ it "CM621" $+ "Foo χρῆν" ==-> "<p>Foo χρῆν</p>\n"+ it "CM622" $+ "Multiple spaces" ==-> "<p>Multiple spaces</p>\n"+ -- NOTE I don't test these so extensively because they share+ -- implementation with emphasis and strong emphasis which are thoroughly+ -- tested already.+ context "strikeout" $ do+ it "works in simplest form" $+ "It's ~~bad~~ news." ==->+ "<p>It's <del>bad</del> news.</p>\n"+ it "combines with emphasis" $+ "**It's ~~bad~~** news." ==->+ "<p><strong>It's <del>bad</del></strong> news.</p>\n"+ it "interacts with subscript reasonably (1)" $+ "It's ~~~bad~~ news~." ==->+ "<p>It's <sub><del>bad</del> news</sub>.</p>\n"+ it "interacts with subscript reasonably (2)" $+ "It's ~~~bad~ news~~." ==->+ "<p>It's <del><sub>bad</sub> news</del>.</p>\n"+ context "subscript" $ do+ it "works in simplest form" $+ "It's ~bad~ news." ==->+ "<p>It's <sub>bad</sub> news.</p>\n"+ it "combines with emphasis" $+ "**It's ~bad~** news." ==->+ "<p><strong>It's <sub>bad</sub></strong> news.</p>\n"+ context "superscript" $ do+ it "works in simplest form" $+ "It's ^bad^ news." ==->+ "<p>It's <sup>bad</sup> news.</p>\n"+ it "combines with emphasis" $+ "**It's ^bad^** news." ==->+ "<p><strong>It's <sup>bad</sup></strong> news.</p>\n"+ it "a composite, complex example" $+ "***Something ~~~is not~~ going~ ^so well^** today*." ==->+ "<p><em><strong>Something <sub><del>is not</del> going</sub> <sup>so well</sup></strong> today</em>.</p>\n"+ context "multiple parse errors" $+ it "they are reported in correct order" $ do+ let s = "Foo `\n\nBar `.\n"+ pe = ueib <> etok '`' <> elabel "code span content"+ s ~~->+ [ err (posN 5 s) pe+ , err (posN 13 s) pe ]+ context "given a complete, comprehensive document" $+ it "outputs expected the HTML fragment" $+ withFiles "data/comprehensive.md" "data/comprehensive.html"+ describe "parseErrorsPretty" $+ it "renders parse errors correctly" $ do+ let s = "Foo\nBar\nBaz\n"+ e0 = err posI (utok 'F' <> etok 'Z')+ e1 = err (posN 4 s) (utok 'B' <> etok 'Z')+ e2 = err (posN 8 s) (utok 'B' <> etok 'Z')+ MMark.parseErrorsPretty s (e0:|[e1,e2]) `shouldBe`+ "1:1:\n |\n1 | Foo\n | ^\nunexpected 'F'\nexpecting 'Z'\n2:1:\n |\n2 | Bar\n | ^\nunexpected 'B'\nexpecting 'Z'\n3:1:\n |\n3 | Baz\n | ^\nunexpected 'B'\nexpecting 'Z'\n"+ describe "useExtension" $+ it "applies given extension" $ do+ doc <- mkDoc "Here we go."+ toText (MMark.useExtension (append_ext "..") doc) `shouldBe`+ "<p>Here we go...</p>\n"+ describe "useExtensions" $+ it "applies extensions in the right order" $ do+ doc <- mkDoc "Here we go."+ let exts =+ [ append_ext "3"+ , append_ext "2"+ , append_ext "1" ]+ toText (MMark.useExtensions exts doc) `shouldBe`+ "<p>Here we go.123</p>\n"+ describe "runScanner and scanner" $+ it "extracts information from markdown document" $ do+ doc <- mkDoc "Here we go, pals."+ let n = MMark.runScanner doc (length_scan (const True))+ n `shouldBe` 17+ describe "combining of scanners" $+ it "combines scanners" $ do+ doc <- mkDoc "Here we go, pals."+ let scan = (,,)+ <$> length_scan (const True)+ <*> length_scan isSpace+ <*> length_scan isPunctuation+ r = MMark.runScanner doc scan+ r `shouldBe` (17, 3, 2)+ describe "projectYaml" $ do+ context "when document does not contain a YAML section" $+ it "returns Nothing" $ do+ doc <- mkDoc "Here we go."+ MMark.projectYaml doc `shouldBe` Nothing+ context "when document contains a YAML section" $ do+ context "when it is valid" $+ it "returns the YAML section" $ do+ doc <- mkDoc "---\nx: 100\ny: 200\n---Here we go."+ let r = object+ [ "x" .= Number 100+ , "y" .= Number 200 ]+ MMark.projectYaml doc `shouldBe` Just r+ context "when it is invalid" $+ it "signal correct parse error" $+ let s = "---\nx: 100\ny: x:\n---Here we go."+ in s ~-> errFancy (posN 15 s)+ (fancy . ErrorCustom . YamlParseError $+ "mapping values are not allowed in this context")++----------------------------------------------------------------------------+-- Testing extensions++-- | Append given text to all 'Plain' blocks.++append_ext :: Text -> MMark.Extension+append_ext y = Ext.inlineTrans $ \case+ Plain x -> Plain (x <> y)+ other -> other++----------------------------------------------------------------------------+-- Testing scanners++-- | Scan total number of characters satisfying a predicate in all 'Plain'+-- inlines.++length_scan :: (Char -> Bool) -> L.Fold (Ext.Block (NonEmpty Inline)) Int+length_scan p = Ext.scanner 0 $ \n block ->+ getSum $ Sum n <> foldMap (foldMap f) block+ where+ f (Plain txt) = (Sum . T.length) (T.filter p txt)+ f _ = mempty++----------------------------------------------------------------------------+-- For testing with documents loaded externally++-- | Load a complete markdown document from an external file and compare the+-- final HTML rendering with contents of another file.++withFiles+ :: FilePath -- ^ Markdown document+ -> FilePath -- ^ HTML document containing the correct result+ -> Expectation+withFiles input output = do+ i <- TIO.readFile input+ o <- TIO.readFile output+ i ==-> o++----------------------------------------------------------------------------+-- Helpers++-- | Unexpected end of inline block.++ueib :: Ord t => ET t+ueib = ulabel "end of inline block"++-- | Expecting end of inline block.++eeib :: Ord t => ET t+eeib = elabel "end of inline block"++-- | Expecting end of URI literal.++euri :: Ord t => ET t+euri = elabel "end of URI literal"++-- | Expecting the rest of path piece.++eppi :: Ord t => ET t+eppi = elabel "the rest of path piece"++-- | Expecting inline content.++eic :: Ord t => ET t+eic = elabel "inline content"++-- | Expecting rest of inline content. Eric!++eric :: Ord t => ET t+eric = elabel "the rest of inline content"++-- | Create a error component complaining that the given 'Text' is not in+-- left- or right- flanking position.++nonFlanking :: Text -> EF MMarkErr+nonFlanking = fancy . ErrorCustom . NonFlankingDelimiterRun . NE.fromList . T.unpack