{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
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
pretty :: Doc -> Text
pretty (Doc blks) = prettyBlocks blks
prettyBlocks :: [Block] -> Text
prettyBlocks blks = runReader (renderBlocks blks) startState
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
SpecialChar c : rest@(x : _) | c `elem` [':', ';'] && not (isSmileyStr x) ->
T.singleton c <> prettyInlines rest
[SpecialChar c] | c `elem` [':', ';'] ->
T.singleton c
(x:xs) ->
renderInline x <> prettyInlines xs
where
startsWithAlphaNum t = case T.uncons t of
Just (c, _) -> isAlphaNum c
_ -> False
isSmileyStr = \case
Str x | x `elem` ["D", ")", "(", "P"] -> True
_ -> False
data PrinterState = PrinterState
{ stateInTable :: Bool
, stateListLevel :: Text
}
type JiraPrinter a = Reader PrinterState a
withDefault :: JiraPrinter a -> a
withDefault = flip runReader startState
startState :: PrinterState
startState = PrinterState
{ stateInTable = False
, stateListLevel = ""
}
renderBlocks :: [Block] -> JiraPrinter Text
renderBlocks = concatBlocks <=< mapM renderBlock
concatBlocks :: [Text] -> JiraPrinter Text
concatBlocks = return . T.intercalate "\n"
appendNewline :: Text -> JiraPrinter Text
appendNewline text = do
listLevel <- asks stateListLevel
inTable <- asks stateInTable
return $
if inTable || not (T.null listLevel)
then text
else text <> "\n"
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)
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 -> '#'
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
listItemToJira :: [Block]
-> JiraPrinter Text
listItemToJira items = do
contents <- renderBlocks items
marker <- asks stateListLevel
return $ case items of
List{} : _ -> contents
_ -> marker <> " " <> contents
renderInline :: Inline -> Text
renderInline = \case
Anchor name -> "{anchor:" <> name <> "}"
AutoLink url -> urlText url
ColorInline color ils -> "{color:" <> colorText color <> "}" <>
prettyInlines ils <> "{color}"
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
'\\' -> "\"
_ -> "\\" `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 -> '^'
urlText :: URL -> Text
urlText (URL url) = url
renderWrapped :: Char -> [Inline] -> Text
renderWrapped c = T.cons c . flip T.snoc c . prettyInlines