{-# 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 "![" <> fromString title <> ctrl "](" <> link (fromString uri) <> 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 "") , 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)