pretty-ghci-0.1.0.0: src/Text/PrettyPrint/GHCi/Haddock.hs
{-# LANGUAGE OverloadedStrings #-}
module Text.PrettyPrint.GHCi.Haddock (
prettyPrintHaddock, haddock2Doc,
HaddockPrintConf(..),
defaultHaddockConf,
) where
import System.Terminal.Utils
-- base
import Control.Monad (join)
import Data.String ( fromString )
import Data.Void ( Void, absurd )
import Data.Char ( isSpace )
import Data.List ( dropWhileEnd )
import System.IO ( stdout )
-- haddock-library
import Documentation.Haddock.Markup
import Documentation.Haddock.Parser
import Documentation.Haddock.Types
-- prettyprinter, prettyprinter-ansi-terminal
import Data.Text.Prettyprint.Doc
import Data.Text.Prettyprint.Doc.Render.Terminal
-- | Given a Haddock-formatted docstring, format and print that docstring to
-- the terminal.
--
-- The 'Bool' is to enable a slower but potentially smarter layout algorithm.
prettyPrintHaddock :: Bool -> String -> IO ()
prettyPrintHaddock smarter str = do
termSize <- getTerminalSize
let layoutOpts = LayoutOptions (AvailablePerLine (maybe 80 snd termSize) 1.0)
layoutAlgo = if smarter then layoutSmart else layoutPretty
renderIO stdout (layoutAlgo layoutOpts (haddock2Doc str))
-- | Parse a docstring into a pretty 'Doc'. Should never throw an exception
-- (since @haddock-library@ will parse /something/ out of any input).
haddock2Doc :: String -> Doc AnsiStyle
haddock2Doc doc = (blocksToDoc blks <> hardline)
where
MetaDoc { _doc = parsedDoc } = parseParas Nothing doc
blks = getAsBlocks $ markup (terminalMarkup defaultHaddockConf) parsedDoc
-- | A Good Enough colour scheme
defaultHaddockConf :: HaddockPrintConf
defaultHaddockConf = HaddockPrintConf
{ hpc_default = mempty
, hpc_emphasis = italicized
, hpc_bold = bold
, hpc_monospaced = colorDull Magenta
, hpc_header = bold <> underlined <> color White
, hpc_identifier = underlined <> color Magenta
, hpc_math = italicized <> color Green
, hpc_links = underlined <> color Blue
, hpc_warning = italicized <> color Red
, hpc_control = bold <> colorDull Yellow
}
-- | Options for how to colour the terminal output
data HaddockPrintConf = HaddockPrintConf
{ hpc_default :: AnsiStyle
-- ^ the default background
, hpc_emphasis :: AnsiStyle
-- ^ emphasized text
, hpc_bold :: AnsiStyle
-- ^ bold text
, hpc_monospaced :: AnsiStyle
-- ^ code blocks and inline code
, hpc_header :: AnsiStyle
-- ^ header bodies
, hpc_identifier :: AnsiStyle
-- ^ identifiers, module links, anchors
, hpc_math :: AnsiStyle
-- ^ @\\( ... \\)@ and @\\[ ... \\]@ delimited math
, hpc_links :: AnsiStyle
-- ^ the link part of hyperlinks or images
, hpc_warning :: AnsiStyle
-- ^ warning texts
, hpc_control :: AnsiStyle
-- ^ things like: equals in headers, bullets in lists, etc.
}
type ReflowSpaces = Bool
-- | The main complexity with turning Haddock's 'DocH' into a
-- @'Doc' 'AnsiStyle'@ is that there is often a conflation of
-- inline and block-level elements.
--
-- We cheat by choosing a worker which tries both at once.
data RenderedDocH = RDH
{ getAsBlocks :: [Doc AnsiStyle] -- ^ render as blocks
, getAsInline :: ReflowSpaces -> Doc AnsiStyle -- ^ render as inline
}
-- | Concatenate a bunch of blocks vertically with empty lines between blocks
blocksToDoc :: [Doc AnsiStyle] -> Doc AnsiStyle
blocksToDoc = align . vcat . punctuate hardline
-- | Markup for turning a 'DocH' into a 'RenderedDocH'
terminalMarkup :: HaddockPrintConf -> DocMarkupH Void Identifier RenderedDocH
terminalMarkup hpc = Markup
{ markupEmpty = RDH { getAsBlocks = []
, getAsInline = mempty }
-- This is where reflow spaces matters: we only split the string into words
-- and glue those words back together if we have the go-ahead to reflow.
, markupString = \str -> onlyInline $ \reflowSpaces -> case str of
"" -> mempty
_ | reflowSpaces
-> let headSpace = if isSpace (head str) then space else mempty
lastSpace = if isSpace (last str) then space else mempty
in headSpace <> fillSep (map fromString (words str)) <> lastSpace
| otherwise
-> fromString (dropWhileEnd (== '\n') str)
, markupParagraph = \doc -> onlyBlock (getAsInline doc True)
, markupAppend = \(RDH b1 i1) (RDH b2 i2) -> RDH (b1 ++ b2) (i1 <> i2)
, markupIdentifier = \(_,idnt,_) -> onlyInline $ \_ -> ident (fromString idnt)
, markupModule = \mdl -> onlyInline $ \_ -> ident (fromString mdl)
, markupAName = \anc -> onlyInline $ \_ -> ident (fromString anc)
, markupIdentifierUnchecked = absurd
, markupEmphasis = \doc -> onlyInline (emph . getAsInline doc)
, markupBold = \doc -> onlyInline (bolded . getAsInline doc)
, markupMonospaced = \doc -> onlyInline (mono . getAsInline doc)
, markupWarning = \doc ->
onlyBlock (warn (getAsInline doc True))
, markupUnorderedList = \docs ->
onlyBlock (renderListLike (repeat "*")
(map (blocksToDoc . getAsBlocks) docs))
, markupOrderedList = \docs ->
onlyBlock (renderListLike [ unsafeViaShow i <> "." | i <- [1 :: Int ..] ]
(map (blocksToDoc . getAsBlocks) docs))
, markupDefList = \lblDocs -> let (lbls, docs) = unzip lblDocs in
onlyBlock (renderListLike [ ctrl (getAsInline l True <> ":") <> hardline | l <- lbls ]
(map (\doc -> align (getAsInline doc False)) docs))
-- Render markdown-style only when we have a title
, markupHyperlink = \(Hyperlink uri titleOpt) -> onlyInline $ \_ -> case titleOpt of
Nothing -> ctrl "<" <> link (fromString uri) <> ctrl ">"
Just title -> ctrl "[" <> fromString title <> ctrl "](" <> link (fromString uri) <> ctrl ")"
-- Render markdown-style only when we have a title
, markupPic = \(Picture uri titleOpt) -> onlyInline $ \_ -> case titleOpt of
Nothing -> ctrl "<<" <> link (fromString uri) <> ctrl ">>"
Just title -> ctrl " <> ctrl ")"
, markupMathInline = \tex -> onlyInline $ \_ ->
math ("\\(" <+> fillSep (map fromString (words tex)) <+> "\\)")
, markupMathDisplay = \tex -> onlyBlock (math ("\\[" <> fromString tex <> "\\]"))
, markupProperty = \prop -> onlyBlock (ctrl "prop>" <> fromString prop)
, markupHeader = \(Header lvl title) -> let leader = ctrl (fromString (replicate lvl '=')) in
onlyBlock (leader <+> header (getAsInline title True))
-- TODO: figure out a good way to render this
, markupTable = \_ -> onlyBlock (bad "<table could not be rendered>")
, markupExample = \examples -> onlyBlock . vcat . join $
[ (ctrl ">>>" <+> fromString input) : (map (mono . fromString) output)
| Example input output <- examples ]
-- This is where we ask for an inline block with spaces /not/ reflowed
, markupCodeBlock = \doc -> RDH { getAsBlocks = [mono (getAsInline doc False)]
, getAsInline = \_ -> mono (getAsInline doc True) }
}
where
-- This element is really and inline one, so interpretting it as a block
-- is a best effort.
onlyInline :: (ReflowSpaces -> Doc AnsiStyle) -> RenderedDocH
onlyInline renderInline = RDH { getAsBlocks = [renderInline False]
, getAsInline = renderInline }
-- This element is really a block one, so interpretting it as inline is a
-- best effort.
onlyBlock :: Doc AnsiStyle -> RenderedDocH
onlyBlock renderBlock = RDH { getAsBlocks = [renderBlock]
, getAsInline = \_ -> align renderBlock }
-- Given what the bullets look like and elements associated with the
-- bullets, produce the doc.
renderListLike :: [Doc AnsiStyle] -> [Doc AnsiStyle] -> Doc AnsiStyle
renderListLike ixs docs = indent 2 . blocksToDoc $
[ ctrl ix <+> doc | (ix,doc) <- ixs `zip` docs ]
-- Useful annotations
header = annotate (hpc_header hpc)
emph = annotate (hpc_emphasis hpc)
bolded = annotate (hpc_bold hpc)
ctrl = annotate (hpc_control hpc)
link = annotate (hpc_links hpc)
math = annotate (hpc_math hpc)
mono = annotate (hpc_monospaced hpc)
ident = annotate (hpc_identifier hpc)
warn = annotate (hpc_warning hpc)
bad = annotate (color Red <> bold <> bgColor White)