packages feed

jira-wiki-markup 0.1.1 → 1.0.0

raw patch · 16 files changed

+1036/−172 lines, 16 filesdep +mtlPVP ok

version bump matches the API change (PVP)

Dependencies added: mtl

API changes (from Hackage documentation)

- Text.Jira.Markup: Deleted :: [Inline] -> Inline
- Text.Jira.Markup: Emph :: [Inline] -> Inline
- Text.Jira.Markup: Inserted :: [Inline] -> Inline
- Text.Jira.Parser.Inline: deleted :: JiraParser Inline
- Text.Jira.Parser.Inline: emph :: JiraParser Inline
- Text.Jira.Parser.Inline: inserted :: JiraParser Inline
- Text.Jira.Parser.Inline: strong :: JiraParser Inline
- Text.Jira.Parser.Inline: subscript :: JiraParser Inline
- Text.Jira.Parser.Inline: superscript :: JiraParser Inline
- Text.Jira.Parser.Inline: symbol :: JiraParser Inline
+ Text.Jira.Markup: AutoLink :: URL -> Inline
+ Text.Jira.Markup: Color :: ColorName -> [Block] -> Block
+ Text.Jira.Markup: ColorName :: Text -> ColorName
+ Text.Jira.Markup: Doc :: [Block] -> Doc
+ Text.Jira.Markup: Emoji :: Icon -> Inline
+ Text.Jira.Markup: Emphasis :: InlineStyle
+ Text.Jira.Markup: HorizontalRule :: Block
+ Text.Jira.Markup: IconAttention :: Icon
+ Text.Jira.Markup: IconCheckmark :: Icon
+ Text.Jira.Markup: IconFlag :: Icon
+ Text.Jira.Markup: IconFlagOff :: Icon
+ Text.Jira.Markup: IconFrowning :: Icon
+ Text.Jira.Markup: IconInfo :: Icon
+ Text.Jira.Markup: IconMinus :: Icon
+ Text.Jira.Markup: IconOff :: Icon
+ Text.Jira.Markup: IconOn :: Icon
+ Text.Jira.Markup: IconPlus :: Icon
+ Text.Jira.Markup: IconQuestionmark :: Icon
+ Text.Jira.Markup: IconSlightlySmiling :: Icon
+ Text.Jira.Markup: IconSmiling :: Icon
+ Text.Jira.Markup: IconStar :: Icon
+ Text.Jira.Markup: IconStarBlue :: Icon
+ Text.Jira.Markup: IconStarGreen :: Icon
+ Text.Jira.Markup: IconStarRed :: Icon
+ Text.Jira.Markup: IconStarYellow :: Icon
+ Text.Jira.Markup: IconThumbsDown :: Icon
+ Text.Jira.Markup: IconThumbsUp :: Icon
+ Text.Jira.Markup: IconTongue :: Icon
+ Text.Jira.Markup: IconWinking :: Icon
+ Text.Jira.Markup: IconX :: Icon
+ Text.Jira.Markup: Insert :: InlineStyle
+ Text.Jira.Markup: SpecialChar :: Char -> Inline
+ Text.Jira.Markup: Strikeout :: InlineStyle
+ Text.Jira.Markup: Styled :: InlineStyle -> [Inline] -> Inline
+ Text.Jira.Markup: [fromDoc] :: Doc -> [Block]
+ Text.Jira.Markup: data Icon
+ Text.Jira.Markup: data InlineStyle
+ Text.Jira.Markup: iconText :: Icon -> Text
+ Text.Jira.Markup: instance GHC.Classes.Eq Text.Jira.Markup.ColorName
+ Text.Jira.Markup: instance GHC.Classes.Eq Text.Jira.Markup.Doc
+ Text.Jira.Markup: instance GHC.Classes.Eq Text.Jira.Markup.Icon
+ Text.Jira.Markup: instance GHC.Classes.Eq Text.Jira.Markup.InlineStyle
+ Text.Jira.Markup: instance GHC.Classes.Ord Text.Jira.Markup.ColorName
+ Text.Jira.Markup: instance GHC.Classes.Ord Text.Jira.Markup.Doc
+ Text.Jira.Markup: instance GHC.Classes.Ord Text.Jira.Markup.Icon
+ Text.Jira.Markup: instance GHC.Classes.Ord Text.Jira.Markup.InlineStyle
+ Text.Jira.Markup: instance GHC.Enum.Enum Text.Jira.Markup.Icon
+ Text.Jira.Markup: instance GHC.Show.Show Text.Jira.Markup.ColorName
+ Text.Jira.Markup: instance GHC.Show.Show Text.Jira.Markup.Doc
+ Text.Jira.Markup: instance GHC.Show.Show Text.Jira.Markup.Icon
+ Text.Jira.Markup: instance GHC.Show.Show Text.Jira.Markup.InlineStyle
+ Text.Jira.Markup: newtype ColorName
+ Text.Jira.Markup: newtype Doc
+ Text.Jira.Parser: doc :: JiraParser Doc
+ Text.Jira.Parser.Block: color :: JiraParser Block
+ Text.Jira.Parser.Block: horizontalRule :: JiraParser Block
+ Text.Jira.Parser.Core: parameters :: JiraParser (Maybe Text, [Parameter])
+ Text.Jira.Parser.Inline: autolink :: JiraParser Inline
+ Text.Jira.Parser.Inline: dash :: JiraParser Inline
+ Text.Jira.Parser.Inline: emoji :: JiraParser Inline
+ Text.Jira.Parser.Inline: specialChar :: JiraParser Inline
+ Text.Jira.Parser.Inline: specialChars :: String
+ Text.Jira.Parser.Inline: styled :: JiraParser Inline
+ Text.Jira.Parser.PlainText: plainText :: Text -> Either ParseError [Inline]
+ Text.Jira.Parser.Shared: icon :: Parsec Text u Icon
+ Text.Jira.Printer: PrinterState :: Bool -> Text -> PrinterState
+ Text.Jira.Printer: [stateInTable] :: PrinterState -> Bool
+ Text.Jira.Printer: [stateListLevel] :: PrinterState -> Text
+ Text.Jira.Printer: data PrinterState
+ Text.Jira.Printer: pretty :: Doc -> Text
+ Text.Jira.Printer: prettyBlocks :: [Block] -> Text
+ Text.Jira.Printer: prettyInlines :: [Inline] -> Text
+ Text.Jira.Printer: renderBlock :: Block -> JiraPrinter Text
+ Text.Jira.Printer: renderInline :: Inline -> Text
+ Text.Jira.Printer: startState :: PrinterState
+ Text.Jira.Printer: type JiraPrinter a = Reader PrinterState a
+ Text.Jira.Printer: withDefault :: JiraPrinter a -> a
- Text.Jira.Markup: Image :: URL -> Inline
+ Text.Jira.Markup: Image :: [Parameter] -> URL -> Inline
- Text.Jira.Markup: Strong :: [Inline] -> Inline
+ Text.Jira.Markup: Strong :: InlineStyle
- Text.Jira.Markup: Subscript :: [Inline] -> Inline
+ Text.Jira.Markup: Subscript :: InlineStyle
- Text.Jira.Markup: Superscript :: [Inline] -> Inline
+ Text.Jira.Markup: Superscript :: InlineStyle
- Text.Jira.Parser: parse :: Text -> Either ParseError [Block]
+ Text.Jira.Parser: parse :: Text -> Either ParseError Doc

Files

CHANGELOG.md view
@@ -3,6 +3,34 @@ `jira-wiki-markup` uses [PVP Versioning][1]. The changelog is available [on GitHub][2]. +1.0.0+=====++Released 2019-12-17.++* Add `Doc` datatype representing a full document; `parse` now returns+  this type.+* Improve parsing:+  - double-backslash is recognized as linebreak;+  - emoticons are parsed as `Emoji`;+  - special sequences of dashes are translated into their unicode+    representation;+  - naked URLs are parsed as `AutoLink`;+  - blocks of colored text are parsed as `Color`;+  - interpretation of special characters as markup can be forced by+    surrounding them with curly braces.+* A parser `plainText` was added available to read markup-less text.+* *Inline*-parser `symbol` was renamed to `specialChar`.+* Add printer module to render the document AST as Jira markup.+* Markup datatype changes:+  - new *Block* elements `Color` and `HorizontalRule`.+  - new *Inline* elements `Emoji`, and `Styled`.+  - *Inline* constructors `Subscript`, `Superscript`, `Emph`, `Strong`,+    `Inserted`, and `Deleted` have been remove. Use `Styled` instead.+  - Constructor `Image` now takes a list of parameters as an additional+    argument.+* CI runs also test GHC 8.8.+ 0.1.1 ===== 
README.md view
@@ -7,9 +7,11 @@ [![Build status](https://travis-ci.com/tarleb/jira-wiki-markup.svg?branch=master)](https://travis-ci.com/tarleb/jira-wiki-markup) [![Windows build status](https://ci.appveyor.com/api/projects/status/github/tarleb/jira-wiki-markup?branch=master&svg=true)](https://ci.appveyor.com/project/tarleb/jira-wiki-markup) -This package provides a parser for [Jira wiki markup]. It converts the-raw text into an abstract syntax tree. The tree is easy to handle and to-different to different output formats.+This package provides a parser and printer for [Jira wiki markup]. It+converts the raw text into an abstract syntax tree. The tree is easy to+handle and to translate into different output formats.++[Jira wiki markup]: https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all  # License 
jira-wiki-markup.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.0 name:                jira-wiki-markup-version:             0.1.1+version:             1.0.0 synopsis:            Handle Jira wiki markup description:         Parse jira wiki text into an abstract syntax tree for easy                      transformation to other formats.@@ -15,7 +15,11 @@ build-type:          Simple extra-doc-files:     README.md                    , CHANGELOG.md-tested-with:         GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.4+tested-with:         GHC == 8.0.2+                   , GHC == 8.2.2+                   , GHC == 8.4.4+                   , GHC == 8.6.4+                   , GHC == 8.8.1  source-repository head   type:                git@@ -28,8 +32,12 @@                      , Text.Jira.Parser.Block                      , Text.Jira.Parser.Core                      , Text.Jira.Parser.Inline+                     , Text.Jira.Parser.PlainText+                     , Text.Jira.Parser.Shared+                     , Text.Jira.Printer    build-depends:       base    >= 4.9   && < 5+                     , mtl     >= 2.2   && < 2.3                      , parsec  >= 3.1   && < 3.2                      , text    >= 1.1.1 && < 1.3 @@ -50,7 +58,6 @@   build-depends:       base    >= 4.9   && < 5                      , text    >= 1.1.1 && < 1.3                      , jira-wiki-markup-                         ghc-options:         -Wall                        -threaded@@ -72,6 +79,7 @@   other-modules:       Text.Jira.ParserTests                      , Text.Jira.Parser.BlockTests                      , Text.Jira.Parser.InlineTests+                     , Text.Jira.PrinterTests    build-depends:       base    >= 4.9   && < 5                      , jira-wiki-markup
src/Text/Jira/Markup.hs view
@@ -11,42 +11,61 @@ Jira markup types. -} module Text.Jira.Markup-  ( Block (..)+  ( Doc (..)+  , Block (..)   , Inline (..)+  , InlineStyle (..)   , ListStyle (..)   , URL (..)+  , ColorName (..)+  , Icon (..)   , Row (..)   , Cell (..)   , Language (..)   , Parameter (..)   , normalizeInlines+  , iconText   ) where  import Data.Text (Text, append) +-- | Jira document+newtype Doc = Doc { fromDoc :: [Block] }+  deriving (Eq, Ord, Show)+ -- | Inline Jira markup elements. data Inline-  = Anchor Text              -- ^ anchor for internal links-  | Deleted [Inline]         -- ^ deleted (struk-out) text-  | Emph [Inline]            -- ^ emphasized text-  | Entity Text              -- ^ named or numeric HTML entity-  | Image URL                -- ^ an image-  | Inserted [Inline]        -- ^ text marked as having been inserted-  | Linebreak                -- ^ hard linebreak-  | Link [Inline] URL        -- ^ hyperlink with alias-  | Monospaced [Inline]      -- ^ text rendered with monospaced font-  | Str Text                 -- ^ simple, markup-less string-  | Space                    -- ^ space between words-  | Strong [Inline]          -- ^ strongly emphasized text-  | Subscript [Inline]       -- ^ subscript text-  | Superscript [Inline]     -- ^ superscript text+  = Anchor Text                         -- ^ anchor for internal links+  | AutoLink URL                        -- ^ URL which is also a link+  | Emoji Icon                          -- ^ emoticon+  | Entity Text                         -- ^ named or numeric HTML entity+  | Image [Parameter] URL               -- ^ an image+  | Linebreak                           -- ^ hard linebreak+  | Link [Inline] URL                   -- ^ hyperlink with alias+  | Monospaced [Inline]                 -- ^ text rendered with monospaced font+  | Space                               -- ^ space between words+  | SpecialChar Char                    -- ^ single char with special meaning+  | Str Text                            -- ^ simple, markup-less string+  | Styled InlineStyle [Inline]         -- ^ styled text   deriving (Eq, Ord, Show) +-- | Supported inline text effect styles.+data InlineStyle+  = Emphasis                            -- ^ emphasized text+  | Insert                              -- ^ text marked as having been inserted+  | Strikeout                           -- ^ deleted (struk-out) text+  | Strong                              -- ^ strongly emphasized text+  | Subscript                           -- ^ subscript text+  | Superscript                         -- ^ superscript text+  deriving (Eq, Ord, Show)+ -- | Blocks of text. data Block   = Code Language [Parameter] Text      -- ^ Code block with panel parameters+  | Color ColorName [Block]             -- ^ text displayed in a specific color   | BlockQuote [Block]                  -- ^ Block of quoted content   | Header Int [Inline]                 -- ^ Header with level and text+  | HorizontalRule                      -- ^ horizontal ruler   | List ListStyle [[Block]]            -- ^ List   | NoFormat [Parameter] Text           -- ^ Unformatted text   | Panel [Parameter] [Block]           -- ^ Formatted panel@@ -56,11 +75,15 @@  -- | Style used for list items. data ListStyle-  = CircleBullets            -- ^ List with round bullets-  | SquareBullets            -- ^ List with square bullets-  | Enumeration              -- ^ Enumeration, i.e., numbered items+  = CircleBullets                       -- ^ List with round bullets+  | SquareBullets                       -- ^ List with square bullets+  | Enumeration                         -- ^ Enumeration, i.e., numbered items   deriving (Eq, Ord, Show) +-- | Text color+newtype ColorName = ColorName Text+  deriving (Eq, Ord, Show)+ -- | Unified resource location newtype URL = URL { fromURL :: Text }   deriving (Eq, Ord, Show)@@ -85,6 +108,33 @@   , parameterValue :: Text   } deriving (Eq, Ord, Show) +-- | Graphical emoticons+data Icon+  = IconSlightlySmiling+  | IconFrowning+  | IconTongue+  | IconSmiling+  | IconWinking+  | IconThumbsUp+  | IconThumbsDown+  | IconInfo+  | IconCheckmark+  | IconX+  | IconAttention+  | IconPlus+  | IconMinus+  | IconQuestionmark+  | IconOn+  | IconOff+  | IconStar+  | IconStarRed+  | IconStarGreen+  | IconStarBlue+  | IconStarYellow+  | IconFlag+  | IconFlagOff+  deriving (Enum, Eq, Ord, Show)+ -- | Normalize a list of inlines, merging elements where possible. normalizeInlines :: [Inline] -> [Inline] normalizeInlines = \case@@ -96,3 +146,30 @@   Linebreak : Space : xs -> Linebreak : normalizeInlines xs   Str s1 : Str s2 : xs   -> Str (s1 `append` s2) : normalizeInlines xs   x : xs                 -> x : normalizeInlines xs++-- | Gets the characters used to represent an emoji.+iconText :: Icon -> Text+iconText = \case+  IconSlightlySmiling -> ":)"+  IconFrowning        -> ":("+  IconTongue          -> ":P"+  IconSmiling         -> ":D"+  IconWinking         -> ";)"+  IconThumbsUp        -> "(y)"+  IconThumbsDown      -> "(n)"+  IconInfo            -> "(i)"+  IconCheckmark       -> "(/)"+  IconX               -> "(x)"+  IconAttention       -> "(!)"+  IconPlus            -> "(+)"+  IconMinus           -> "(-)"+  IconQuestionmark    -> "(?)"+  IconOn              -> "(on)"+  IconOff             -> "(off)"+  IconStar            -> "(*)"+  IconStarRed         -> "(*r)"+  IconStarGreen       -> "(*g)"+  IconStarBlue        -> "(*b)"+  IconStarYellow      -> "(*y)"+  IconFlag            -> "(flag)"+  IconFlagOff         -> "(flagoff)"
src/Text/Jira/Parser.hs view
@@ -11,10 +11,12 @@ -} module Text.Jira.Parser   ( parse+  , doc   , module Text.Jira.Markup   , module Text.Jira.Parser.Core   , module Text.Jira.Parser.Inline   , module Text.Jira.Parser.Block+  , module Text.Jira.Parser.PlainText   ) where  import Data.Text (Text)@@ -22,8 +24,13 @@ import Text.Jira.Parser.Block import Text.Jira.Parser.Core import Text.Jira.Parser.Inline+import Text.Jira.Parser.PlainText import Text.Parsec hiding (parse) --- | Parses a document into a list of blocks.-parse :: Text -> Either ParseError [Block]-parse = parseJira (many1 block)+-- | Parses a document into a Jira AST.+parse :: Text -> Either ParseError Doc+parse = parseJira doc++-- | Parses a list of jira blocks into a @'Doc'@ element.+doc :: JiraParser Doc+doc = Doc <$> many block <?> "doc"
src/Text/Jira/Parser/Block.hs view
@@ -14,7 +14,9 @@     -- * Parsers for block types   , blockQuote   , code+  , color   , header+  , horizontalRule   , list   , noformat   , panel@@ -37,9 +39,11 @@   , list   , table   , blockQuote+  , horizontalRule   , code   , noformat   , panel+  , color   , para   ] <* skipWhitespace @@ -47,8 +51,9 @@ para :: JiraParser Block para = (<?> "para") . try $ do   isInList <- stateInList <$> getState-  when isInList $+  when isInList $ do     notFollowedBy' blankline+    notFollowedBy' horizontalRule   Para . normalizeInlines <$> many1 inline  -- | Parses a header line into a @Header@.@@ -88,14 +93,14 @@     firstItemAtDepth :: Int -> JiraParser [Block]     firstItemAtDepth depth = try $ listContent (depth + 1) <|>       do-        blocks <- nonListContent+        blocks <- nonListContent depth         nestedLists <- try . many $ listAtDepth (depth + 1)         return $ blocks ++ nestedLists      listItemAtDepth :: Int -> JiraParser Char -> JiraParser [Block]     listItemAtDepth depth bulletChar = atDepth depth *>-        try (bulletChar *> nonListContent) <|>-        try (anyBulletMarker *> listContent depth)+      (try (bulletChar *> nonListContent depth) <|>+       try (anyBulletMarker *> listContent depth))      listContent :: Int -> JiraParser [Block]     listContent depth = do@@ -106,13 +111,14 @@     anyBulletMarker :: JiraParser Char     anyBulletMarker = oneOf "*-#" -    nonListContent :: JiraParser [Block]-    nonListContent = try $+    nonListContent :: Int -> JiraParser [Block]+    nonListContent depth = try $       let nonListBlock = notFollowedBy' (many1 (oneOf "#-*")) *> block       in char ' ' *> do-        first <- block-        rest  <- many nonListBlock-        return (first : rest)+        first   <- block+        nonList <- many nonListBlock+        lists   <- many (listAtDepth (depth + 1))+        return (first : nonList ++ lists)  -- | Parses a table into a @Table@ element. table :: JiraParser Block@@ -146,9 +152,12 @@ -- | Parses a code block into a @Code@ element. code :: JiraParser Block code = try $ do-  (lang, params) <- string "{code" *> parameters <* char '}' <* blankline+  (langName, params) <- string "{code" *> parameters <* char '}' <* blankline+  let lang = maybe defaultLanguage Language langName   content <- anyChar `manyTill` try (string "{code}" *> blankline)   return $ Code lang params (pack content)+  where+    defaultLanguage = Language (pack "java")  -- | Parses a block quote into a @'Quote'@ element. blockQuote :: JiraParser Block@@ -161,6 +170,11 @@                   (string "{quote}" *> optional blankline *>                    block `manyTill` try (string "{quote}")) +-- | Parses four consecutive hyphens as @'HorizontalRule'@.+horizontalRule :: JiraParser Block+horizontalRule = HorizontalRule <$+  try (string "----" *> blankline)+ -- | Parses a preformatted text into a @NoFormat@ element. noformat :: JiraParser Block noformat = try $ do@@ -175,19 +189,15 @@   content <- block `manyTill` try (string "{panel}" *> blankline)   return $ Panel params content --- | Parses a set of panel parameters-parameters :: JiraParser (Language, [Parameter])-parameters = option (defaultLanguage, []) $ do-  _      <- char ':'-  lang   <- option defaultLanguage (try language)-  params <- try (Parameter <$> key <*> (char '=' *> value)) `sepBy` pipe-  return (lang, params)+-- | Parses colored text into a @'Color'@ element.+color :: JiraParser Block+color= try $ do+  name <- string "{color:" *> (colorName <|> colorCode) <* char '}'+  content <- block `manyTill` try (string "{color}" *> blankline)+  return $ Color (ColorName $ pack name) content   where-    defaultLanguage = Language (pack "java")-    pipe     = char '|'-    key      = pack <$> many1 (noneOf "\"'\t\n\r |{}=")-    value    = pack <$> many1 (noneOf "\"'\n\r|{}=")-    language = Language <$> key <* (pipe <|> lookAhead (char '}'))+    colorName = many letter+    colorCode = optional (char '#') *> count 6 digit  -- | Skip whitespace till we reach the next block skipWhitespace :: JiraParser ()
src/Text/Jira/Parser/Core.hs view
@@ -26,10 +26,12 @@   , blankline   , skipSpaces   , blockNames+  , parameters   ) where  import Control.Monad (join, void)-import Data.Text (Text)+import Data.Text (Text, pack)+import Text.Jira.Markup import Text.Parsec  -- | Jira Parsec parser@@ -88,22 +90,37 @@ blankline :: JiraParser () blankline = try $ skipSpaces *> void newline +-- | Parses a set of panel parameters+parameters :: JiraParser (Maybe Text, [Parameter])+parameters = option (Nothing, []) $ do+  _      <- char ':'+  lang   <- optionMaybe (try language)+  params <- try (Parameter <$> key <*> (char '=' *> value)) `sepBy` pipe+  return (lang, params)+  where+    pipe     = char '|'+    key      = pack <$> many1 (noneOf "\"'\t\n\r |{}=")+    value    = pack <$> many1 (noneOf "\"'\n\r|{}=")+    language = key <* (pipe <|> lookAhead (char '}'))+ -- | Succeeds if the parser is looking at the end of a paragraph. endOfPara :: JiraParser () endOfPara = eof   <|> lookAhead blankline   <|> lookAhead headerStart+  <|> lookAhead horizontalRule   <|> lookAhead listItemStart   <|> lookAhead tableStart   <|> lookAhead panelStart   where-    headerStart   = void $ char 'h' *> oneOf "123456" <* char '.'-    listItemStart = void $ many1 (oneOf "#*-") *> char ' '-    tableStart    = void $ skipSpaces *> many1 (char '|') *> char ' '-    panelStart    = void $ char '{' *> choice (map string blockNames)+    headerStart    = void $ char 'h' *> oneOf "123456" <* char '.'+    listItemStart  = void $ many1 (oneOf "#*-") <* char ' '+    tableStart     = void $ skipSpaces *> many1 (char '|') *> char ' '+    panelStart     = void $ char '{' *> choice (map (try . string) blockNames)+    horizontalRule = void $ try (string "----") *> blankline  blockNames :: [String]-blockNames = ["code", "noformat", "panel", "quote"]+blockNames = ["code", "color", "noformat", "panel", "quote"]  -- | Variant of parsec's @notFollowedBy@ function which properly fails even if -- the given parser does not consume any input (like @eof@ does).
src/Text/Jira/Parser/Inline.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-} {-| Module      : Text.Jira.Parser.Inline Copyright   : © 2019 Albert Krewinkel@@ -14,64 +16,67 @@   ( inline     -- * Inline component parsers   , anchor-  , deleted-  , emph+  , autolink+  , dash+  , emoji   , entity-  , inserted   , image   , linebreak   , link   , monospaced+  , specialChar   , str-  , strong-  , subscript-  , superscript-  , symbol+  , styled   , whitespace+    -- * Constants+  , specialChars   ) where  import Control.Monad (guard, void)-import Data.Char (isLetter)+import Data.Char (isLetter, isPunctuation, ord)+#if !MIN_VERSION_base(4,13,0) import Data.Monoid ((<>), All (..))-import Data.Text (pack, singleton)+#else+import Data.Monoid (All (..))+#endif+import Data.Text (append, pack) import Text.Jira.Markup import Text.Jira.Parser.Core+import Text.Jira.Parser.Shared import Text.Parsec  -- | Parses any inline element. inline :: JiraParser Inline inline = notFollowedBy' blockEnd *> choice   [ whitespace+  , emoji+  , dash+  , autolink   , str   , linebreak   , link   , image-  , emph-  , strong-  , subscript-  , superscript-  , deleted-  , inserted+  , styled   , monospaced   , anchor   , entity-  , symbol+  , specialChar   ] <?> "inline"   where     blockEnd = char '{' *> choice (map string blockNames) <* char '}' --- | Characters with a special meaning, i.e., those used for markup.+-- | Characters which, depending on context, can have a special meaning. specialChars :: String-specialChars = " \n" ++ symbolChars---- | Special characters which can be part of a string.-symbolChars :: String-symbolChars = "_+-*^~|[]{}!&\\"+specialChars = "_+-*^~|[]{}(!&\\" --- | Parses an in-paragraph newline as a @Linebreak@ element.+-- | Parses an in-paragraph newline as a @Linebreak@ element. Both newline+-- characters and double-backslash are recognized as line-breaks. linebreak :: JiraParser Inline-linebreak = Linebreak <$ try (newline <* notFollowedBy' endOfPara)-  <?> "linebreak"+linebreak = Linebreak <$ try (+  choice [ void $ newline <* notFollowedBy' endOfPara+         , void $ string "\\\\" <* notFollowedBy' (char '\\')+         ]+  ) <?> "linebreak"  -- | Parses whitespace and return a @Space@ element. whitespace :: JiraParser Inline@@ -81,9 +86,11 @@ str :: JiraParser Inline str = Str . pack <$> (alphaNums <|> otherNonSpecialChars) <?> "string"   where+    nonStrChars = " \n" ++ specialChars     alphaNums = many1 alphaNum <* updateLastStrPos-    otherNonSpecialChars = many1 (noneOf specialChars)+    otherNonSpecialChars = many1 (noneOf nonStrChars) + -- | Parses an HTML entity into an @'Entity'@ element. entity :: JiraParser Inline entity = Entity . pack@@ -92,21 +99,34 @@     numerical = (:) <$> char '#' <*> many1 digit     named = many1 letter +-- | Parses textual representation of an icon into an @'Emoji'@ element.+emoji :: JiraParser Inline+emoji = Emoji <$> icon <* notFollowedBy' letter <?> "emoji"++-- | Parses ASCII representation of en-dash or em-dash.+dash :: JiraParser Inline+dash = try $ do+  guard =<< notAfterString+  _ <- string "--"+  choice [ Str "—" <$ char '-'   -- em dash+         , pure (Str "–")          -- en dash+         ] <* lookAhead (void (char ' ') <|> eof)+ -- | Parses a special character symbol as a @Str@.-symbol :: JiraParser Inline-symbol = Str . singleton <$> (escapedChar <|> symbolChar)-  <?> "symbol"+specialChar :: JiraParser Inline+specialChar = SpecialChar <$> (escapedChar <|> plainSpecialChar)+  <?> "special char"   where-    escapedChar = try (char '\\' *> oneOf symbolChars)+    escapedChar = try (char '\\' *> satisfy isPunctuation) -    symbolChar = do+    plainSpecialChar = do       inTablePred <- do         b <- stateInTable <$> getState         return $ if b then All . (/= '|') else mempty       inLinkPred  <- do         b <- stateInLink  <$> getState         return $ if b then All . (`notElem` ("]|\n" :: String)) else mempty-      oneOf $ filter (getAll . (inTablePred <> inLinkPred)) symbolChars+      oneOf $ filter (getAll . (inTablePred <> inLinkPred)) specialChars   --@@ -120,8 +140,12 @@  -- | Parse image into an @Image@ element. image :: JiraParser Inline-image = fmap (Image . URL . pack) . try $-  char '!' *> noneOf "\r\t\n" `manyTill` char '!'+image = try $ do+  -- does not use @url@, as is may contain relative locations.+  src <- char '!' *> (URL . pack <$> many1 (noneOf "\r\t\n|]!"))+  (_, params) <- option (Nothing, []) (char '|' *> parameters)+  _ <- char '!'+  return $ Image params src  -- | Parse link into a @Link@ element. link :: JiraParser Inline@@ -129,43 +153,80 @@   guard . not . stateInLink =<< getState   withStateFlag (\b st -> st { stateInLink = b }) $ do     _ <- char '['-    alias <- option [] $ try (many inline <* char '|')-    url   <- URL . pack <$> many1 (noneOf "|] \n")+    alias   <- option [] $ try (many inline <* char '|')+    linkUrl <- email <|> url     _ <- char ']'-    return $ Link alias url+    return $ Link alias linkUrl +autolink :: JiraParser Inline+autolink = AutoLink <$> (email <|> url) <?> "email or other URL"++-- | Parse a URL with scheme @file@, @ftp@, @http@, @https@, @irc@, @nntp@, or+-- @news@.+url :: JiraParser URL+url = try $ do+  urlScheme <- scheme+  sep <- pack <$> string "://"+  rest <- pack <$> many urlChar+  return $ URL (urlScheme `append` sep `append` rest)+  where+    scheme = do+      first <- letter+      case first of+        'f' -> ("file" <$ string "ile") <|> ("ftp" <$ string "tp")+        'h' -> string "ttp" *> option "http" ("https" <$ char 's')+        'i' -> "irc" <$ string "rc"+        'n' -> ("nntp" <$ string "ntp") <|> ("news" <$ string "ews")+        _   -> fail "not looking at a known scheme"++-- | Parses an E-mail URL.+email :: JiraParser URL+email = URL . pack <$> try+  ((++) <$> string "mailto:" <*> many1 urlChar)++-- | Parses a character which is allowed in URLs+urlChar :: JiraParser Char+urlChar = satisfy $ \c ->+  c `notElem` ("|]" :: String) && ord c >= 32 && ord c <= 127++ -- -- Markup ----- | Parses deleted text into @Deleted@.-deleted :: JiraParser Inline-deleted = Deleted <$> ('-' `delimitingMany` inline) <?> "deleted" --- | Parses emphasized text into @Emph@.-emph :: JiraParser Inline-emph = Emph <$> ('_' `delimitingMany` inline) <?> "emphasis"+-- | Parses styled text+styled :: JiraParser Inline+styled = (simpleStyled <|> forceStyled) <?> "styled text"+  where+    simpleStyled = try $ do+      styleChar <- lookAhead $ oneOf "-_+*~^"+      content   <- styleChar `delimitingMany` inline+      let style = delimiterStyle styleChar+      return $ Styled style content --- | Parses inserted text into @Inserted@.-inserted :: JiraParser Inline-inserted = Inserted <$> ('+' `delimitingMany` inline) <?> "inserted"+    forceStyled = try $ do+      styleChar <- char '{' *> oneOf "-_+*~^" <* char '}'+      let closing = try $ string ['{', styleChar, '}']+      let style   = delimiterStyle styleChar+      content   <- manyTill inline closing+      return $ Styled style content +-- | Returns the markup kind from the delimiting markup character.+delimiterStyle :: Char -> InlineStyle+delimiterStyle = \case+  '*' -> Strong+  '+' -> Insert+  '-' -> Strikeout+  '^' -> Superscript+  '_' -> Emphasis+  '~' -> Subscript+  c   -> error ("Unknown delimiter character: " ++ [c])+ -- | Parses monospaced text into @Monospaced@. monospaced :: JiraParser Inline monospaced = Monospaced   <$> enclosed (try $ string "{{") (try $ string "}}") inline   <?> "monospaced"---- | Parses strongly emphasized text into @Strong@.-strong :: JiraParser Inline-strong = Strong <$> ('*' `delimitingMany` inline) <?> "strong"---- | Parses subscript text into @Subscript@.-subscript :: JiraParser Inline-subscript = Subscript <$> ('~' `delimitingMany` inline) <?> "subscript"---- | Parses superscript text into @Superscript@.-superscript :: JiraParser Inline-superscript = Superscript <$> ('^' `delimitingMany` inline) <?> "superscript"  -- -- Helpers
+ src/Text/Jira/Parser/PlainText.hs view
@@ -0,0 +1,46 @@+{-|+Module      : Text.Jira.Parser.PlainText+Copyright   : © 2019 Albert Krewinkel+License     : MIT++Maintainer  : Albert Krewinkel <tarleb@zeitkraut.de>+Stability   : alpha+Portability : portable++Functions for parsing markup-less strings.+-}++module Text.Jira.Parser.PlainText+  ( plainText+  ) where++import Data.Text (Text, append, pack)+import Text.Jira.Markup+import Text.Jira.Parser.Core+import Text.Jira.Parser.Inline (specialChars)+import Text.Jira.Parser.Shared (icon)+import Text.Parsec++-- | Parses into an @'Inline'@ elements which represent plain text. The+-- result consists of any number of @'Str'@, @'SpecialChar'@, or+-- @'Space'@ elements.+--+-- This parser can be used to convert un-escaped strings into proper+-- Jira markup elements.+plainText :: Text -> Either ParseError [Inline]+plainText = parseJira (normalizeInlines <$> many plainInlineParser)+  where+    plainInlineParser :: JiraParser Inline+    plainInlineParser = choice+      [ Space <$ skipMany1 (char ' ')+      , escapeIcon+      , plainSpecialChar+      , Str . pack <$> many1 (noneOf (' ':specialChars))+      ] <?> "text-only inline"++-- | Escapes text which would otherwise render as an icon.+escapeIcon :: Parsec Text u Inline+escapeIcon = Str . ("\\" `append`) . iconText <$> icon++plainSpecialChar :: Parsec Text u Inline+plainSpecialChar = SpecialChar <$> oneOf specialChars
+ src/Text/Jira/Parser/Shared.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE LambdaCase #-}+{-|+Module      : Text.Jira.Parser.Shared+Copyright   : © 2019 Albert Krewinkel+License     : MIT++Maintainer  : Albert Krewinkel <tarleb@zeitkraut.de>+Stability   : alpha+Portability : portable++Parsers whch are shared between multiple modules.+-}+module Text.Jira.Parser.Shared+  ( icon+  ) where++import Data.Char (isLetter)+import Data.Text (Text)+import Text.Jira.Markup+import Text.Parsec++-- | Parses an icon+icon :: Parsec Text u Icon+icon = smiley <|> otherIcon++smiley :: Parsec Text u Icon+smiley = try $ choice+  [ IconWinking <$ string ";)"+  , char ':' *> anyChar >>= \case+      'D' -> pure IconSmiling+      ')' -> pure IconSlightlySmiling+      '(' -> pure IconFrowning+      'P' -> pure IconTongue+      c   -> fail ("unknown smiley: :" ++ [c])+  ]++otherIcon :: Parsec Text u Icon+otherIcon = try $ do+  let isIconChar c = isLetter c || (c `elem` ("/!+-?*" :: String))+  name <- char '('+          *> many1 (satisfy isIconChar)+          <* char ')'+  case name of+    "y"       -> pure IconThumbsUp+    "n"       -> pure IconThumbsDown+    "i"       -> pure IconInfo+    "/"       -> pure IconCheckmark+    "x"       -> pure IconX+    "!"       -> pure IconAttention+    "+"       -> pure IconPlus+    "-"       -> pure IconMinus+    "?"       -> pure IconQuestionmark+    "on"      -> pure IconOn+    "off"     -> pure IconOff+    "*"       -> pure IconStar+    "*r"      -> pure IconStarRed+    "*g"      -> pure IconStarGreen+    "*b"      -> pure IconStarBlue+    "*y"      -> pure IconStarYellow+    "flag"    -> pure IconFlag+    "flagoff" -> pure IconFlagOff+    _         -> fail ("not a known emoji" ++ name)
+ src/Text/Jira/Printer.hs view
@@ -0,0 +1,252 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-|+Module      : Text.Jira.Parser+Copyright   : © 2019 Albert Krewinkel+License     : MIT++Maintainer  : Albert Krewinkel <tarleb@zeitkraut.de>+Stability   : alpha+Portability : portable++Generate Jira wiki markup text from an abstract syntax tree.+-}+module Text.Jira.Printer+  ( pretty+  , renderBlock+  , renderInline+  , prettyBlocks+  , prettyInlines+  , JiraPrinter+  , PrinterState (..)+  , startState+  , withDefault+  ) where++import Data.Char (isAlphaNum)+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid ((<>))+#endif+import Control.Monad ((<=<))+import Control.Monad.Reader (Reader, runReader, asks, local)+import Data.Text (Text)+import Text.Jira.Markup+import qualified Data.Text as T++-- | Render Jira document as Jira wiki formatted text.+pretty :: Doc -> Text+pretty (Doc blks) = prettyBlocks blks++-- | Render a list of Jira blocks as Jira wiki formatted text.+prettyBlocks :: [Block] -> Text+prettyBlocks blks = runReader (renderBlocks blks) startState++-- | Renders a list of Jira inline markup elements.+prettyInlines :: [Inline] -> Text+prettyInlines = \case+  [] ->+    ""+  s@Str{} : Styled style inlns : rest ->+    renderInline s <> renderStyledSafely style inlns <> prettyInlines rest+  Styled style inlns : s@(Str t) : rest | startsWithAlphaNum t ->+    renderStyledSafely style inlns <> renderInline s <> prettyInlines rest+  s@Str{} : SpecialChar c : rest@(Str {}:_) ->+    (renderInline s `T.snoc` c) <> prettyInlines rest+  s@Space : SpecialChar c : rest@(Space {}:_) ->+    (renderInline s `T.snoc` c) <> prettyInlines rest+  s@Linebreak : SpecialChar c : rest@(Space {}:_) ->+    (renderInline s `T.snoc` c) <> prettyInlines rest+  (x:xs) ->+    renderInline x <> prettyInlines xs++  where+    startsWithAlphaNum t = case T.uncons t of+      Just (c, _) -> isAlphaNum c+      _           -> False++-- | Internal state used by the printer.+data PrinterState = PrinterState+  { stateInTable   :: Bool+  , stateListLevel :: Text+  }++type JiraPrinter a = Reader PrinterState a++-- | Run with default state.+withDefault :: JiraPrinter a -> a+withDefault = flip runReader startState++-- | Default start state of the printer.+startState :: PrinterState+startState = PrinterState+  { stateInTable = False+  , stateListLevel = ""+  }++-- | Render a block as Jira wiki format.+renderBlocks :: [Block] -> JiraPrinter Text+renderBlocks = concatBlocks <=< mapM renderBlock++-- | Combine the texts produced from rendering a list of blocks.+concatBlocks :: [Text] -> JiraPrinter Text+concatBlocks = return . T.intercalate "\n"++-- | Add a newline character unless we are within a list or table.+appendNewline :: Text -> JiraPrinter Text+appendNewline text = do+  listLevel <- asks stateListLevel+  inTable   <- asks stateInTable+  return $+    -- add final newline only if we are neither within a table nor a list.+    if inTable || not (T.null listLevel)+    then text+    else text <> "\n"++-- | Render a block as Jira wiki format.+renderBlock :: Block -> JiraPrinter Text+renderBlock = \case+  Code lang params content -> return $ T.concat+                              [ "{code:"+                              , T.intercalate "|"+                                (renderLang lang : map renderParam params)+                              , "}\n"+                              , content+                              , "\n{code}"+                              ]+  Color colorName blocks   -> renderBlocks blocks >>= \blks -> return $ T.concat+                              [ "{color:", colorText colorName, "}\n"+                              , blks+                              , "{color}"+                              ]+  BlockQuote [Para xs]     -> return $ "bq. " <> prettyInlines xs+  BlockQuote blocks        -> renderBlocks blocks >>= \blks -> return $ T.concat+                              [ "{quote}\n"+                              , blks+                              , "\n{quote}"]+  Header lvl inlines       -> return $ T.concat+                              [ "h",  T.pack (show lvl), ". "+                              , prettyInlines inlines+                              ]+  HorizontalRule           -> return "----"+  List style items         -> listWithMarker items (styleChar style) >>=+                              appendNewline+  NoFormat params content  -> return $ T.concat+                              [ "{noformat"+                              , renderBlockParams params+                              , "}\n"+                              , content+                              , "{noformat}"+                              ]+  Panel params blocks     -> renderBlocks blocks >>= \blks ->+                             return $ T.concat+                             [ "{panel"+                             , renderBlockParams params+                             , "}\n"+                             , blks+                             , "{panel}"+                             ]+  Para inlines              -> appendNewline $ prettyInlines inlines+  Table rows                ->+    local (\st -> st { stateInTable = True }) $+      fmap T.unlines (mapM renderRow rows)++-- | Returns the ext representation of a color+colorText :: ColorName -> Text+colorText (ColorName c) = c++renderLang :: Language -> Text+renderLang (Language lang) = lang++renderBlockParams :: [Parameter] -> Text+renderBlockParams = \case+  [] -> mempty+  xs -> T.cons ':' (renderParams xs)++renderParams :: [Parameter] -> Text+renderParams = T.intercalate "|" . map renderParam++renderParam :: Parameter -> Text+renderParam (Parameter key value) = key <> "=" <> value++renderRow :: Row -> JiraPrinter Text+renderRow (Row cells) = do+  rendered <- mapM renderCell cells+  let closing = if all isHeaderCell cells then " ||" else " |"+  return $ T.unwords rendered <> closing+  where+    isHeaderCell HeaderCell {} = True+    isHeaderCell BodyCell {}   = False++renderCell :: Cell -> JiraPrinter Text+renderCell cell = let (cellStart, blocks) = case cell of+                        (HeaderCell bs) -> ("|| ", bs)+                        (BodyCell bs) -> ("| ", bs)+                  in (cellStart <>) <$> renderBlocks blocks++styleChar :: ListStyle -> Char+styleChar = \case+  CircleBullets -> '*'+  SquareBullets -> '-'+  Enumeration   -> '#'++-- | Create a list using the given character as bullet item marker.+listWithMarker :: [[Block]]+               -> Char+               -> JiraPrinter Text+listWithMarker items marker = do+  let addItem s = s { stateListLevel = stateListLevel s `T.snoc` marker }+  renderedBlocks <- local addItem $ mapM listItemToJira items+  return $ T.intercalate "\n" renderedBlocks++-- | Convert bullet or ordered list item (list of blocks) to Jira.+listItemToJira :: [Block]+               -> JiraPrinter Text+listItemToJira items = do+  contents <- renderBlocks items+  marker <- asks stateListLevel+  return $ case items of+    List{} : _ -> contents+    _          -> marker <> " " <> contents++-- | Renders a single inline item as Jira markup.+renderInline :: Inline -> Text+renderInline = \case+  Anchor name            -> "{anchor:" <> name <> "}"+  AutoLink url           -> urlText url+  Emoji icon             -> iconText icon+  Entity entity          -> "&" <> entity <> ";"+  Image params url       -> "!" <> urlText url <>+                            if null params+                            then "!"+                            else "|" <> renderParams params <> "!"+  Linebreak              -> "\n"+  Link inlines (URL url) -> "[" <> prettyInlines inlines <> "|" <> url <> "]"+  Monospaced inlines     -> "{{" <> prettyInlines inlines <> "}}"+  Space                  -> " "+  SpecialChar c          -> case c of+                              -- backslash is unescapable, render as entity+                              '\\' -> "&bsol;"+                              _    -> "\\" `T.snoc` c+  Str txt                -> txt+  Styled style inlines   -> renderWrapped (delimiterChar style) inlines++renderStyledSafely :: InlineStyle -> [Inline] -> Text+renderStyledSafely style =+  let delim = T.pack ['{', delimiterChar style, '}']+  in (delim <>) . (<> delim) . prettyInlines++delimiterChar :: InlineStyle -> Char+delimiterChar = \case+  Emphasis -> '_'+  Insert -> '+'+  Strong -> '*'+  Strikeout -> '-'+  Subscript -> '~'+  Superscript -> '^'++-- | Text rendering of an URL.+urlText :: URL -> Text+urlText (URL url) = url++renderWrapped :: Char -> [Inline] -> Text+renderWrapped c = T.cons c . flip T.snoc c . prettyInlines
test/Text/Jira/Parser/BlockTests.hs view
@@ -45,6 +45,10 @@       , testCase "ended by blank line" $         parseJira para "Hello\n\n" @?=         Right (Para [Str "Hello"])++      , testCase "deleted text after linebreak" $+        parseJira para "foo\n-deleted-\n" @?=+        Right (Para [Str "foo", Linebreak, Styled Strikeout [Str "deleted"]])       ]      , testGroup "header"@@ -78,6 +82,12 @@         isLeft (parseJira header " h1. nope\n") @? "leading spaces"       ] +    , testGroup "horizontalRule"+      [ testCase "single ruler" $+        parseJira horizontalRule "----\n" @?=+        Right HorizontalRule+      ]+     , testGroup "list"       [ testCase "single item list" $         parseJira list "* hello\n" @?=@@ -157,14 +167,18 @@       , testCase "item after nested list" $         let text = Text.unlines                    [ "* first"-                   , "** nested"                    , "* second"+                   , "** nested1"+                   , "** nested2"+                   , "* third"                    ]         in parseJira list text @?=            Right (List CircleBullets-                  [ [ Para [Str "first"]-                    , List CircleBullets [[Para [Str "nested"]]]]-                  , [ Para [Str "second"]]])+                  [ [ Para [Str "first"] ]+                  , [ Para [Str "second"]+                    , List CircleBullets [ [Para [Str "nested1"]]+                                         , [Para [Str "nested2"]]]]+                  , [ Para [Str "third"]] ])        , testCase "nested lists" $         let text = Text.unlines@@ -287,6 +301,13 @@         Right (Panel [Parameter "title" "test"] [Para [Str "line"]])       ] +    , testGroup "color"+      [ testCase "single paragraph" $+        parseJira color "{color:red}This is red.\n{color}\n" @?=+        Right (Color (ColorName "red")+               [Para [Str "This", Space, Str "is", Space, Str "red."]])+      ]+     , testGroup "blockQuote"       [ testCase "single line quite before eof" $         parseJira blockQuote "bq. this text" @?=@@ -319,6 +340,14 @@     , testCase "para after header" $       parseJira ((,) <$> block <*> block) "h2.header\nparagraph\n" @?=       Right (Header 2 [Str "header"], Para [Str "paragraph"])++    , testCase "para before horizontal rule " $+      parseJira ((,) <$> block <*> return HorizontalRule) "paragraph\n----\n" @?=+      Right (Para [Str "paragraph"], HorizontalRule)++    , testCase "para after horizontal rule " $+      parseJira ((,) <$> block <*> block) "----\nparagraph\n" @?=+      Right (HorizontalRule, Para [Str "paragraph"])      , testCase "para after list" $       parseJira ((,) <$> block <*> block) "* foo\n\nbar\n" @?=
test/Text/Jira/Parser/InlineTests.hs view
@@ -39,14 +39,45 @@         "str should only be parsed into Space"       ] -    , testGroup "symbol"-      [ testCase "special symbol" $-        parseJira symbol "!" @?= Right (Str "!")+    , testGroup "specialChar"+      [ testCase "plain special char" $+        parseJira specialChar "!" @?= Right (SpecialChar '!')        , testCase "escaped symbol" $-        parseJira symbol "\\{" @?= Right (Str "{")+        parseJira specialChar "\\{" @?= Right (SpecialChar '{')       ] +    , testGroup "dash"+      [ testCase "en dash" $+        parseJira dash "--" @?= Right (Str "–")++      , testCase "em dash" $+        parseJira dash "---" @?= Right (Str "—")+      ]++    , testGroup "emoji"+      [ testCase "smiling face" $+        parseJira emoji ":D" @?= Right (Emoji IconSmiling)++      , testCase "winking face" $+        parseJira emoji ";)" @?= Right (Emoji IconWinking)++      , testCase "checkmark" $+        parseJira emoji "(/)" @?= Right (Emoji IconCheckmark)++      , testCase "red x" $+        parseJira emoji "(x)" @?= Right (Emoji IconX)++      , testCase "thumbs up" $+        parseJira emoji "(y)" @?= Right (Emoji IconThumbsUp)++      , testCase "green star" $+        parseJira emoji "(*g)" @?= Right (Emoji IconStarGreen)++      , testCase "may not be followed by a letter" $+        isLeft (parseJira emoji "(x)nope") @? "no letters after emojis"+      ]+     , testGroup "whitespace"       [ testCase "space" $         parseJira whitespace " " @?= Right Space@@ -95,43 +126,58 @@         "entities must not be empty"       ] -    , testCase "deleted" $-      parseJira deleted "-far-fetched-" @?=-      Right (Deleted [Str "far", Str "-", Str "fetched"])+    , testGroup "styled" -    , testGroup "emph"-      [ testCase "single word" $-        parseJira emph "_single_" @?= Right (Emph [Str "single"])+      [ testCase "deleted" $+        parseJira styled "-far-fetched-" @?=+        Right (Styled Strikeout [Str "far", SpecialChar '-', Str "fetched"]) -      , testCase "multi word" $-        parseJira emph "_multiple words_" @?=-        Right (Emph [Str "multiple", Space, Str "words"])+      , testGroup "emphasis"+        [ testCase "single word" $+          parseJira styled "_single_" @?= Right (Styled Emphasis [Str "single"]) -      , testCase "symbol before opening underscore" $-        parseJira (str *> emph) "#_bar_" @?=-        Right (Emph [Str "bar"])+        , testCase "multi word" $+          parseJira styled "_multiple words_" @?=+          Right (Styled Emphasis [Str "multiple", Space, Str "words"]) -      , testCase "neither symbol nor space before opening underscore" $-        isLeft (parseJira (str *> emph) "foo_bar_") @? "space after opening char"+        , testCase "forced markup" $+          parseJira styled "{_}forced{_}" @?=+          Right (Styled Emphasis [Str "forced"]) -      , testCase "disallow space after opening underscore" $-        isLeft (parseJira emph "_ nope_") @? "space after underscore"+        , testCase "symbol before opening underscore" $+          parseJira (str *> styled) "#_bar_" @?=+          Right (Styled Emphasis [Str "bar"]) -      , testCase "require word boundary after closing underscore" $-        isLeft (parseJira emph "_nope_nope") @? "no boundary after closing"+        , testCase "neither symbol nor space before opening underscore" $+          isLeft (parseJira (str *> styled) "foo_bar_") @? "space after opening char" -      , testCase "zero with space as word boundary" $-        parseJira ((,) <$> emph <*> str) "_yup_\8203next" @?=-        Right (Emph [Str "yup"], Str "\8203next")+        , testCase "disallow space after opening underscore" $+          isLeft (parseJira styled "_ nope_") @? "space after underscore" -      , testCase "fails for strong" $-        isLeft (parseJira emph "*strong*") @? "strong as emph"-      ]+        , testCase "require word boundary after closing underscore" $+          isLeft (parseJira styled "_nope_nope") @? "no boundary after closing" -    , testCase "inserted" $-      parseJira inserted "+multiple words+" @?=-      Right (Inserted [Str "multiple", Space, Str "words"])+        , testCase "zero with space as word boundary" $+          parseJira ((,) <$> styled <*> str) "_yup_\8203next" @?=+          Right (Styled Emphasis [Str "yup"], Str "\8203next")+        ] +      , testCase "inserted" $+        parseJira styled "+multiple words+" @?=+        Right (Styled Insert [Str "multiple", Space, Str "words"])++      , testCase "strong" $+        parseJira styled "*single*" @?= Right (Styled Strong [Str "single"])++      , testCase "subscript" $+        parseJira styled "~multiple words~" @?=+        Right (Styled Subscript [Str "multiple", Space, Str "words"])++      , testCase "superscript" $+        parseJira styled "^multiple words^" @?=+        Right (Styled Superscript [Str "multiple", Space, Str "words"])+    ]+     , testCase "monospaced" $       parseJira monospaced "{{multiple words}}" @?=       Right (Monospaced [Str "multiple", Space, Str "words"])@@ -141,6 +187,10 @@         parseJira linebreak "\na" @?=         Right Linebreak +      , testCase "double-backslash linebreak" $+        parseJira linebreak "\\\\" @?=+        Right Linebreak+       , testCase "linebreak at eof fails" $         isLeft (parseJira linebreak "\n") @? "newline before eof" @@ -152,32 +202,25 @@        , testCase "linebreak before header fails" $         isLeft (parseJira linebreak "\nh1.foo\n") @? "newline before header"-      ] -    , testGroup "strong"-      [ testCase "single word" $-        parseJira strong "*single*" @?= Right (Strong [Str "single"])--      , testCase "multi word" $-        parseJira strong "*multiple words*" @?=-        Right (Strong [Str "multiple", Space, Str "words"])--      , testCase "fails for emph" $-        isLeft (parseJira strong "_emph_") @? "emph as strong"+      , testCase "three backslashes do not cause a linebreak" $+        isLeft (parseJira linebreak "\\\\\\") @? "three backslashes"       ] -    , testCase "subscript" $-      parseJira subscript "~multiple words~" @?=-      Right (Subscript [Str "multiple", Space, Str "words"])--    , testCase "superscript" $-      parseJira superscript "^multiple words^" @?=-      Right (Superscript [Str "multiple", Space, Str "words"])-     , testCase "anchor" $       parseJira anchor "{anchor:testing}" @?=       Right (Anchor "testing") +    , testGroup "autolink"+      [ testCase "hypertext link" $+        parseJira autolink "https://example.org/foo" @?=+        Right (AutoLink (URL "https://example.org/foo"))++      , testCase "email" $+        parseJira autolink "mailto:nobody@test.invalid" @?=+        Right (AutoLink (URL "mailto:nobody@test.invalid"))+      ]+     , testGroup "link"       [ testCase "unaliased link" $         parseJira link "[https://example.org]" @?=@@ -189,14 +232,19 @@        , testCase "alias with emphasis" $         parseJira link "[_important_ example|https://example.org]" @?=-        Right (Link [Emph [Str "important"], Space, Str "example"]+        Right (Link [Styled Emphasis [Str "important"], Space, Str "example"]                 (URL "https://example.org"))++      , testCase "mail address" $+        parseJira link "[send mail|mailto:me@nope.invalid]" @?=+        Right (Link [Str "send", Space, Str "mail"]+               (URL "mailto:me@nope.invalid"))       ]      , testGroup "image"       [ testCase "local file" $         parseJira image "!image.jpg!" @?=-        Right (Image (URL "image.jpg"))+        Right (Image [] (URL "image.jpg"))        , testCase "no newlines" $         isLeft (parseJira image "!hello\nworld.png!") @?@@ -207,15 +255,45 @@   , testGroup "inline parser"     [ testCase "simple sentence" $       parseJira (normalizeInlines <$> many1 inline) "Hello, World!" @?=-      Right [Str "Hello,", Space, Str "World!"]+      Right [Str "Hello,", Space, Str "World", SpecialChar '!']      , testCase "with entity" $       parseJira (many1 inline) "shopping at P&amp;C" @?=       Right [ Str "shopping", Space, Str "at", Space-            , Str "P", Entity "amp", Str "C"]+            , Str "P", Entity "amp", Str "C"+            ] +    , testCase "autolink followed by pipe" $+      parseJira (many1 inline) "https://jira.example/file.txt|" @?=+      Right [AutoLink (URL "https://jira.example/file.txt"), SpecialChar '|']+     , testCase "backslash-escaped char" $       parseJira (normalizeInlines <$> many1 inline) "opening brace: \\{" @?=-      Right [ Str "opening", Space, Str "brace:", Space, Str "{"]+      Right [Str "opening", Space, Str "brace:", Space, SpecialChar '{']++    , testCase "icon after word" $+      parseJira (many1 inline) "checkmark(/)" @?=+      Right [Str "checkmark", Emoji IconCheckmark]++    , testCase "smiley after word" $+      parseJira (normalizeInlines <$> many1 inline) "smiley:)" @?=+      Right [Str "smiley", Emoji IconSlightlySmiling]++    , testCase "escaped smiley after word" $+      parseJira (normalizeInlines <$> many1 inline) "closing paren\\:)" @?=+      Right [Str "closing", Space, Str "paren", SpecialChar ':', Str ")"]++    , testCase "smiley between words" $+      parseJira (normalizeInlines <$> many1 inline) "verdict: :D funny" @?=+      Right [Str "verdict:", Space, Emoji IconSmiling, Space, Str "funny"]++    , testCase "dash with spaces" $+      parseJira (many1 inline) "one  -- two" @?=+      Right [Str "one", Space, Str "–", Space, Str "two"]+++    , testCase "forced markup" $+      parseJira (many1 inline) "H{~}2{~}O" @?=+      Right [Str "H", Styled Subscript [Str "2"], Str "O"]     ]   ]
test/Text/Jira/ParserTests.hs view
@@ -13,6 +13,8 @@  import Data.Text () import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase, (@?=))+import Text.Jira.Parser import qualified Text.Jira.Parser.BlockTests import qualified Text.Jira.Parser.InlineTests @@ -20,4 +22,36 @@ tests = testGroup "Parser"   [ Text.Jira.Parser.InlineTests.tests   , Text.Jira.Parser.BlockTests.tests+  , testGroup "doc"+    [ testCase "empty document" $+      parse "" @?=+      Right (Doc [])++    , testCase "simple document" $+      parse "h1. test\nThis is ok." @?=+      Right (Doc [ Header 1 [Str "test"]+                 , Para [Str "This", Space, Str "is", Space, Str "ok."]])+    ]++  , testGroup "plainText"+    [ testCase "word" $+      plainText "kthxbye" @?=+      Right [Str "kthxbye"]++    , testCase "words" $+      plainText "be Berlin" @?=+      Right [Str "be", Space, Str "Berlin"]++    , testCase "smiley" $+      plainText ":)" @?=+      Right [Str "\\:)"]++    , testCase "icon after word" $+      plainText "f(x)" @?=+      Right [Str "f\\(x)"]++    , testCase "special chars" $+      plainText "*not strong*" @?=+      Right [SpecialChar '*', Str "not", Space, Str "strong", SpecialChar '*']+    ]   ]
+ test/Text/Jira/PrinterTests.hs view
@@ -0,0 +1,151 @@+{-|+Module      : Text.Jira.PrinterTests+Copyright   : © 2019 Albert Krewinkel+License     : MIT++Maintainer  : Albert Krewinkel <tarleb@zeitkraut.de>+Stability   : alpha+Portability : portable++Tests for the jira wiki printer.+-}+module Text.Jira.PrinterTests (tests) where++import Prelude hiding (unlines)+import Data.Text (Text, unlines)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase, (@?=))+import Text.Jira.Markup+import Text.Jira.Printer++tests :: TestTree+tests = testGroup "Printer"+  [ testGroup "doc"+    [ testCase "empty document" $+      pretty (Doc []) @?= ""+    ]++  , testGroup "blocks"+    [ testCase "simple paragraph" $+      let para = Para [Str "Hello,", Space, Str "World!"]+      in renderBlock' para @?= "Hello, World!\n"++    , testCase "two paragraphs" $+      let para1 = Para [Str "First", Space, Str "paragraph."]+          para2 = Para [Str "Second", Space, Str "paragraph."]+      in prettyBlocks [para1, para2] @?= unlines+         [ "First paragraph."+         , ""+         , "Second paragraph."+         ]++    , testGroup "header"+      [ testCase "simple" $+        let header = Header 1 [Str "test", Space, Str "header"]+        in renderBlock' header @?= "h1. test header"++      , testCase "header" $+        let header = Header 5 [Str "test", Space, Str "header"]+        in renderBlock' header @?= "h5. test header"+      ]++    , testCase "horizontal rule" $+      renderBlock' HorizontalRule @?= "----"++    , testCase "color" $+      renderBlock' (Color (ColorName "blue") [Para [Str "yabadee"]]) @?=+      "{color:blue}\nyabadee\n{color}"++    , testGroup "list"+      [ testCase "simple list" $+        let list = List SquareBullets [ [Para [Str "first"]]+                                      , [Para [Str "second"]]+                                      , [Para [Str "third"]]+                                      ]+        in renderBlock' list @?= unlines+           [ "- first"+           , "- second"+           , "- third"+           ]++      , testCase "nested list" $+        let list = List CircleBullets+                   [ [Para [Str "first"]]+                   , [ List Enumeration   [ [Para [Str "second-1"]]+                                          , [Para [Str "second-2"]]]]+                   , [ Para [Str "third"]+                     , List CircleBullets [ [Para [Str "third-1"]]+                                          , [Para [Str "third-2"]]]]+                   ]+        in renderBlock' list @?= unlines+           [ "* first"+           , "*# second-1"+           , "*# second-2"+           , "* third"+           , "** third-1"+           , "** third-2"+           ]+      ]++    , testCase "table" $+      let headerRow = Row [ HeaderCell [Para [Str "one"]]+                          , HeaderCell [Para [Str "two"]]+                          ]+          bodyRow = Row [ BodyCell [Para [Str "1"]]+                        , BodyCell [Para [Str "2"]]+                        ]+          table = Table [headerRow, bodyRow]+      in renderBlock' table @?= "|| one || two ||\n| 1 | 2 |\n"++    , testCase "para after table" $+      let table = Table [Row [BodyCell [Para [Str "boring"]]]]+          para = Para [Str "after", Space, Str "table"]+      in prettyBlocks [table, para] @?= "| boring |\n\nafter table\n"++    , testCase "para after list" $+      let list = List Enumeration [[Para [Str "boring"]]]+          para = Para [Str "after", Space, Str "table"]+      in prettyBlocks [list, para] @?= "# boring\n\nafter table\n"+    ]++  , testGroup "isolated inline"+    [ testCase "SpecialChar" $+      renderInline (SpecialChar '*') @?= "\\*"++    , testCase "AutoLink" $+      renderInline (AutoLink (URL "https://example.org")) @?=+      "https://example.org"++    , testCase "Emoji" $+      renderInline (Emoji IconSmiling) @?= ":D"++    , testCase "Styled Emphasis" $+      renderInline (Styled Emphasis [Str "Hello,", Space, Str "World!"]) @?=+      "_Hello, World!_"++    , testCase "Styled Strong" $+      renderInline (Styled Strong [Str "Hello,", Space, Str "World!"]) @?=+      "*Hello, World!*"+    ]++  , testGroup "combined inlines"+    [ testCase "special char between words" $+      prettyInlines [Str "easy", SpecialChar '-', Str "peasy"] @?=+      "easy-peasy"++    , testCase "special char before word" $+      prettyInlines [SpecialChar '*', Str "star"] @?=+      "\\*star"++    , testCase "markup within word" $+      prettyInlines [Str "H", Styled Subscript [Str "2"], Str "O"] @?=+      "H{~}2{~}O"++    , testCase "markup followed by punctuation" $+      prettyInlines [Styled Emphasis [Str "Word"], Str "."] @?=+      "_Word_."+    ]+  ]++renderBlock' :: Block -> Text+renderBlock' = withDefault . renderBlock
test/jira-wiki-markup-test.hs view
@@ -14,6 +14,7 @@ import Data.Text () import Test.Tasty (TestTree, defaultMain, testGroup) import qualified Text.Jira.ParserTests+import qualified Text.Jira.PrinterTests  -- | Run the tests for jira-wiki-markup. main :: IO ()@@ -22,4 +23,5 @@ tests :: TestTree tests = testGroup "jira-wiki-markup"   [ Text.Jira.ParserTests.tests+  , Text.Jira.PrinterTests.tests   ]