packages feed

nirum-0.5.0: src/Nirum/Docs/ReStructuredText.hs

{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Nirum.Docs.ReStructuredText (ReStructuredText, render) where

import qualified Data.List.NonEmpty
import Data.List.NonEmpty (NonEmpty (..), toList, (<|))
import Data.Maybe

import qualified Data.Text as T
import Text.InterpolatedString.Perl6 (qq)

import Nirum.Docs

type ReStructuredText = T.Text

renderInline :: Inline -> ReStructuredText
renderInline (Text t) = escape t
renderInline SoftLineBreak = "\n"
renderInline HardLineBreak = "\n"
renderInline (HtmlInline html) = [qq|:raw:`$html`|]
renderInline (Code code') = [qq|``{code'}``|]
renderInline (Emphasis inlines) = [qq|*{escape $ bareText inlines}*|]
renderInline (Strong inlines) = [qq|**{escape $ bareText inlines}**|]
renderInline (Image url title)
  | T.null title = T.concat ["\n\n.. image:: ", url, "\n\n"]
  | otherwise = T.concat ["\n\n.. image:: ", url, "\n   :alt: ", title, "\n\n"]
renderInline (Link url _ inlines)
  | length images < length inlines = [qq|`{escape $ bareText inlines} <$url>`_|]
  | otherwise = T.replace "\n\n\n\n" "\n\n" $ T.concat [image i | i <- images]
  where
    images :: [(T.Text, T.Text)]
    images = [(url', title) | Image url' title <- inlines]
    image :: (T.Text, T.Text) -> ReStructuredText
    image (url', title)
     | T.null title = T.concat [ "\n\n.. image:: ", url', "\n   :target: "
                               , url, "\n\n"
                               ]
     | otherwise = T.concat ["\n\n.. image:: ", url', "\n   :alt: ", title
                            , "\n   :target: ", url, "\n\n"]

bareText :: [Inline] -> T.Text
bareText inlines =
    T.concat $ map t inlines
  where
    t :: Inline -> T.Text
    t (Text t') = t'
    t SoftLineBreak = "\n"
    t HardLineBreak = "\n"
    t (HtmlInline _) = ""
    t (Code code') = code'
    t (Emphasis inlines') = bareText inlines'
    t (Strong inlines') = bareText inlines'
    t (Link _ _ inlines') = bareText inlines'
    t (Image _ _) = ""

escape :: T.Text -> ReStructuredText
escape = T.concatMap escapeChar

escapeChar :: Char -> Html
escapeChar '\\' = "\\\\"
escapeChar ':' = "\\:"
escapeChar '`' = "\\`"
escapeChar '.' = "\\."
escapeChar c = T.singleton c

renderInlines :: [Inline] -> ReStructuredText
renderInlines inlines =
    T.concat $ prependBar $ map renderInline inlines
  where
    useLineblocks :: Bool
    useLineblocks = not $ null [i | i@HardLineBreak <- inlines]
    prependBar :: [ReStructuredText] -> [ReStructuredText]
    prependBar ts = if useLineblocks then "| " : ts else ts

indent :: T.Text -> ReStructuredText -> ReStructuredText
indent spaces =
    T.intercalate "\n" . map indent' . T.lines
  where
    indent' :: T.Text -> T.Text
    indent' line
      | T.null line = T.empty
      | otherwise = spaces `T.append` line

indent2 :: ReStructuredText -> ReStructuredText
indent2 = indent "  "

indent3 :: ReStructuredText -> ReStructuredText
indent3 = indent "   "

indent4 :: ReStructuredText -> ReStructuredText
indent4 = indent "    "

renderBlock :: Block -> ReStructuredText
renderBlock (Document blocks) = renderBlocks blocks `T.snoc` '\n'
renderBlock ThematicBreak = "----------"
renderBlock (Paragraph inlines) = renderInlines inlines
renderBlock (BlockQuote blocks) = indent4 (renderBlocks blocks)
renderBlock (HtmlBlock html) =
    T.concat [ ".. raw:: html\n\n"
             , indent3 html
             ]
renderBlock (CodeBlock lang code') =
    T.concat [ if T.null lang then "::" else [qq|.. code:: $lang|]
             , "\n\n"
             , indent3 code'
             ]
renderBlock (Heading level inlines anchorId) =
    T.concat [ref, text, "\n", T.pack [hChar | _ <- [1 .. (T.length text)]]]
  where
    ref :: ReStructuredText
    ref = case anchorId of
        Nothing -> ""
        Just id' -> T.concat ["\n.. _", id', ":\n\n"]
    text :: ReStructuredText
    text = renderInlines inlines
    hChar :: Char
    hChar = case level of
        H1 -> '='
        H2 -> '-'
        H3 -> '~'
        H4 -> '`'
        H5 -> '.'
        H6 -> '\''
renderBlock (List BulletList (TightItemList items)) =
    T.intercalate "\n" [ [qq|- {T.drop 2 $ indent2 $ renderTightBlocks i}|]
                       | i <- items
                       ]
renderBlock (List BulletList (LooseItemList items)) =
    T.intercalate "\n\n" [ [qq|- {T.drop 2 $ indent2 $ renderBlocks i}|]
                         | i <- items
                         ]
renderBlock (List (OrderedList startNum _) (TightItemList items)) =
    T.intercalate "\n" [ [qq|$n. {T.drop 3 $ indent3 $ renderTightBlocks i}|]
                       | (n, i) <- indexed startNum items
                       ]
renderBlock (List (OrderedList startNum _) (LooseItemList items)) =
    T.intercalate "\n\n" [ [qq|$n. {T.drop 3 $ indent3 $ renderBlocks i}|]
                         | (n, i) <- indexed startNum items
                         ]
renderBlock (Table _ allRows@(header :| rows)) = T.concat $
    ["\n", hline "-", row header, hline "="] ++
    [row cells `T.append` hline "-" | cells <- rows] ++
    ["\n"]
  where
    widths :: NonEmpty Int
    widths = columnWidths allRows
    hline :: T.Text -> ReStructuredText
    hline c = T.concat
        [ "+"
        , T.intercalate "+" [T.replicate (w + 2) c | w <- toList widths]
        , "+\n"
        ]
    row :: TableRow -> ReStructuredText
    row cells = T.concat
        [ T.concat
            [ "|"
            , T.intercalate "|"
                [ T.concat
                    [ " "
                    , T.justifyLeft w ' ' $ case drop lineIdx lines' of
                        line' : _ -> line'
                        [] -> ""
                    , " "
                    ]
                | (lines', w) <- cells'
                ]
            , "|\n"
            ]
        | lineIdx <- [0 .. (rowHeight cells - 1)]
        ]
      where
        cells' :: [([ReStructuredText], Int)]
        cells' =
            [ (T.lines $ renderInlines cell, w)
            | (cell, w) <- toList $ Data.List.NonEmpty.zip cells widths
            ]

cellWidthHeight :: TableCell -> (Int, Int)
cellWidthHeight inlines =
    case T.lines $ renderInlines inlines of
        [] -> (0, 0)
        lines' -> (maximum (map T.length lines'), length lines')

columnWidths :: NonEmpty TableRow -> NonEmpty Int
columnWidths =
    widths . fmap Just
  where
    cellWidths :: NonEmpty (Maybe TableRow) -> NonEmpty Int
    cellWidths =
        fmap (maybe 0 (fst . cellWidthHeight . Data.List.NonEmpty.head))
    restCols :: NonEmpty (Maybe TableRow) -> NonEmpty (Maybe TableRow)
    restCols = fmap $ maybe Nothing
        (Data.List.NonEmpty.nonEmpty . Data.List.NonEmpty.tail)
    widths :: NonEmpty (Maybe TableRow) -> NonEmpty Int
    widths rows =
        let
            restCols' = restCols rows
            maxWidth = maximum (cellWidths rows)
        in
            if any isJust restCols'
            then maxWidth <| widths restCols'
            else maxWidth :| []

rowHeight :: TableRow -> Int
rowHeight =
    maximum . fmap (snd . cellWidthHeight)

indexed :: Enum i => i -> [a] -> [(i, a)]
indexed _ [] = []
indexed start (x : xs) = (start, x) : indexed (succ start) xs

renderBlocks :: [Block] -> ReStructuredText
renderBlocks = T.intercalate "\n\n" . map renderBlock

renderTightBlocks :: [Block] -> ReStructuredText
renderTightBlocks blocks = T.intercalate "\n\n"
    [ case b of
        Paragraph inlines -> renderInlines inlines
        b' -> renderBlock b'
    | b <- blocks
    ]

render :: Block -> ReStructuredText
render = renderBlock