purescript-0.15.15: src/Language/PureScript/Docs/AsHtml.hs
-- | Functions for rendering generated documentation from PureScript code as
-- HTML.
module Language.PureScript.Docs.AsHtml (
HtmlOutput(..),
HtmlOutputModule(..),
HtmlRenderContext(..),
nullRenderContext,
packageAsHtml,
moduleAsHtml,
makeFragment,
renderMarkdown
) where
import Prelude
import Control.Category ((>>>))
import Control.Monad (unless)
import Data.Bifunctor (bimap)
import Data.Char (isUpper)
import Data.Either (isRight)
import Data.List.NonEmpty qualified as NE
import Data.Maybe (fromMaybe)
import Data.Foldable (for_)
import Data.String (fromString)
import Data.Text (Text)
import Data.Text qualified as T
import Text.Blaze.Html5 as H hiding (map)
import Text.Blaze.Html5.Attributes qualified as A
import Cheapskate qualified
import Language.PureScript qualified as P
import Language.PureScript.Docs.Types
import Language.PureScript.Docs.RenderedCode (Link(..), outputWith)
import Language.PureScript.Docs.Render qualified as Render
import Language.PureScript.CST qualified as CST
data HtmlOutput a = HtmlOutput
{ htmlIndex :: [(Maybe Char, a)]
, htmlModules :: [(P.ModuleName, HtmlOutputModule a)]
}
deriving (Show, Functor)
data HtmlOutputModule a = HtmlOutputModule
{ htmlOutputModuleLocals :: a
, htmlOutputModuleReExports :: [(InPackage P.ModuleName, a)]
}
deriving (Show, Functor)
data HtmlRenderContext = HtmlRenderContext
{ buildDocLink :: Namespace -> Text -> ContainingModule -> Maybe DocLink
, renderDocLink :: DocLink -> Text
, renderSourceLink :: P.SourceSpan -> Maybe Text
}
-- |
-- An HtmlRenderContext for when you don't want to render any links.
nullRenderContext :: HtmlRenderContext
nullRenderContext = HtmlRenderContext
{ buildDocLink = const (const (const Nothing))
, renderDocLink = const ""
, renderSourceLink = const Nothing
}
packageAsHtml
:: (InPackage P.ModuleName -> Maybe HtmlRenderContext)
-> Package a
-> HtmlOutput Html
packageAsHtml getHtmlCtx Package{..} =
HtmlOutput indexFile modules
where
indexFile = []
modules = moduleAsHtml getHtmlCtx <$> pkgModules
moduleAsHtml
:: (InPackage P.ModuleName -> Maybe HtmlRenderContext)
-> Module
-> (P.ModuleName, HtmlOutputModule Html)
moduleAsHtml getHtmlCtx Module{..} = (modName, HtmlOutputModule modHtml reexports)
where
modHtml = do
let r = fromMaybe nullRenderContext $ getHtmlCtx (Local modName)
in do
for_ modComments renderMarkdown
for_ modDeclarations (declAsHtml r)
reexports =
flip map modReExports $ \(pkg, decls) ->
let r = fromMaybe nullRenderContext $ getHtmlCtx pkg
in (pkg, foldMap (declAsHtml r) decls)
-- renderIndex :: LinksContext -> [(Maybe Char, Html)]
-- renderIndex LinksContext{..} = go ctxBookmarks
-- where
-- go = takeLocals
-- >>> groupIndex getIndex renderEntry
-- >>> map (second (ul . mconcat))
--
-- getIndex (_, title_) = do
-- c <- textHeadMay title_
-- guard (toUpper c `elem` ['A'..'Z'])
-- pure c
--
-- textHeadMay t =
-- case T.length t of
-- 0 -> Nothing
-- _ -> Just (T.index t 0)
--
-- renderEntry (mn, title_) =
-- li $ do
-- let url = T.pack (filePathFor mn `relativeTo` "index") <> "#" <> title_
-- code $
-- a ! A.href (v url) $ text title_
-- sp
-- text ("(" <> P.runModuleName mn <> ")")
--
-- groupIndex :: Ord i => (a -> Maybe i) -> (a -> b) -> [a] -> [(Maybe i, [b])]
-- groupIndex f g =
-- map (second DList.toList) . M.toList . foldr go' M.empty . sortBy (comparing f)
-- where
-- go' x = insertOrAppend (f x) (g x)
-- insertOrAppend idx val m =
-- let cur = M.findWithDefault DList.empty idx m
-- new = DList.snoc cur val
-- in M.insert idx new m
declAsHtml :: HtmlRenderContext -> Declaration -> Html
declAsHtml r d@Declaration{..} = do
let declFragment = makeFragment (declInfoNamespace declInfo) declTitle
H.div ! A.class_ "decl" ! A.id (v (T.drop 1 declFragment)) $ do
h3 ! A.class_ "decl__title clearfix" $ do
a ! A.class_ "decl__anchor" ! A.href (v declFragment) $ "#"
H.span $ text declTitle
text " " -- prevent browser from treating
-- declTitle + linkToSource as one word
for_ declSourceSpan (linkToSource r)
H.div ! A.class_ "decl__body" $ do
case declInfo of
AliasDeclaration fixity alias_ ->
renderAlias fixity alias_
_ -> do
pre ! A.class_ "decl__signature" $ do
for_ declKind $ \kindInfo -> do
code ! A.class_ "decl__kind" $ do
codeAsHtml r (Render.renderKindSig declTitle kindInfo)
code $ codeAsHtml r (Render.renderDeclaration d)
for_ declComments renderMarkdown
let (instances, dctors, members) = partitionChildren declChildren
unless (null dctors) $ do
h4 "Constructors"
renderChildren r dctors
unless (null members) $ do
h4 "Members"
renderChildren r members
unless (null instances) $ do
h4 "Instances"
renderChildren r instances
where
linkToSource :: HtmlRenderContext -> P.SourceSpan -> Html
linkToSource ctx srcspan =
maybe (return ()) go (renderSourceLink ctx srcspan)
where
go href =
H.span ! A.class_ "decl__source" $
a ! A.href (v href) $ text "Source"
renderChildren :: HtmlRenderContext -> [ChildDeclaration] -> Html
renderChildren _ [] = return ()
renderChildren r xs = ul $ mapM_ item xs
where
item decl =
li ! A.id (v (T.drop 1 (fragment decl))) $ do
renderCode decl
for_ (cdeclComments decl) $ \coms ->
H.div ! A.class_ "decl__child_comments" $ renderMarkdown coms
fragment decl = makeFragment (childDeclInfoNamespace (cdeclInfo decl)) (cdeclTitle decl)
renderCode = code . codeAsHtml r . Render.renderChildDeclaration
codeAsHtml :: HtmlRenderContext -> RenderedCode -> Html
codeAsHtml r = outputWith elemAsHtml
where
elemAsHtml e = case e of
Syntax x ->
withClass "syntax" (text x)
Keyword x ->
withClass "keyword" (text x)
Space ->
text " "
Symbol ns name link_ ->
case link_ of
Link mn ->
let
class_ =
if startsWithUpper name then "ctor" else "ident"
target
| isOp name =
if ns == TypeLevel
then "type (" <> name <> ")"
else "(" <> name <> ")"
| otherwise = name
in
linkToDecl ns target mn (withClass class_ (text name))
NoLink ->
text name
Role role ->
case role of
"nominal" -> renderRole describeNominal "decl__role_nominal"
"phantom" -> renderRole describePhantom "decl__role_phantom"
-- representational is intentionally not rendered
"representational" -> toHtml ("" :: Text)
x -> P.internalError $ "codeAsHtml: unknown value for role annotation: '" <> T.unpack x <> "'"
where
renderRole hoverTextContent className =
H.a ! A.href (v docRepoRolePage) ! A.target (v "_blank") ! A.class_ "decl__role" $ do
H.abbr ! A.class_ "decl__role_hover" ! A.title (v hoverTextContent) $ do
H.sub ! A.class_ className $ do
toHtml ("" :: Text)
docRepoRolePage =
"https://github.com/purescript/documentation/blob/master/language/Roles.md"
describeNominal =
"The 'nominal' role means this argument may not change when coercing the type."
describePhantom =
"The 'phantom' role means this argument can change freely when coercing the type."
linkToDecl = linkToDeclaration r
startsWithUpper :: Text -> Bool
startsWithUpper str = not (T.null str) && isUpper (T.index str 0)
isOp = isRight . runParser CST.parseOperator
runParser :: CST.Parser a -> Text -> Either String a
runParser p' =
bimap (CST.prettyPrintError . NE.head) snd
. CST.runTokenParser p'
. CST.lex
renderLink :: HtmlRenderContext -> DocLink -> Html -> Html
renderLink r link_@DocLink{..} =
a ! A.href (v (renderDocLink r link_ <> fragmentFor link_))
! A.title (v fullyQualifiedName)
where
fullyQualifiedName =
P.runModuleName modName <> "." <> linkTitle
modName = case linkLocation of
LocalModule m -> m
DepsModule _ _ m -> m
BuiltinModule m -> m
makeFragment :: Namespace -> Text -> Text
makeFragment ns = (prefix <>) . escape
where
prefix = case ns of
TypeLevel -> "#t:"
ValueLevel -> "#v:"
-- TODO
escape = id
fragmentFor :: DocLink -> Text
fragmentFor l = makeFragment (linkNamespace l) (linkTitle l)
linkToDeclaration ::
HtmlRenderContext ->
Namespace ->
Text ->
ContainingModule ->
Html ->
Html
linkToDeclaration r ns target containMn =
maybe id (renderLink r) (buildDocLink r ns target containMn)
renderAlias :: P.Fixity -> FixityAlias -> Html
renderAlias (P.Fixity associativity precedence) alias_ =
p $ do
-- TODO: Render a link
toHtml $ "Operator alias for " <> P.showQualified showAliasName alias_ <> " "
em $
text ("(" <> associativityStr <> " / precedence " <> T.pack (show precedence) <> ")")
where
showAliasName (Left valueAlias) = P.runProperName valueAlias
showAliasName (Right typeAlias) = case typeAlias of
(Left identifier) -> P.runIdent identifier
(Right properName) -> P.runProperName properName
associativityStr = case associativity of
P.Infixl -> "left-associative"
P.Infixr -> "right-associative"
P.Infix -> "non-associative"
-- | Render Markdown to HTML. Safe for untrusted input. Relative links are
-- | removed.
renderMarkdown :: Text -> H.Html
renderMarkdown =
H.toMarkup . removeRelativeLinks . Cheapskate.markdown opts
where
opts = Cheapskate.def { Cheapskate.allowRawHtml = False }
removeRelativeLinks :: Cheapskate.Doc -> Cheapskate.Doc
removeRelativeLinks = Cheapskate.walk go
where
go :: Cheapskate.Inlines -> Cheapskate.Inlines
go = (>>= stripRelatives)
stripRelatives :: Cheapskate.Inline -> Cheapskate.Inlines
stripRelatives (Cheapskate.Link contents_ href _)
| isRelativeURI href = contents_
stripRelatives other = pure other
-- Tests for a ':' character in the first segment of a URI.
--
-- See Section 4.2 of RFC 3986:
-- https://tools.ietf.org/html/rfc3986#section-4.2
--
-- >>> isRelativeURI "http://example.com/" == False
-- >>> isRelativeURI "mailto:me@example.com" == False
-- >>> isRelativeURI "foo/bar" == True
-- >>> isRelativeURI "/bar" == True
-- >>> isRelativeURI "./bar" == True
isRelativeURI :: Text -> Bool
isRelativeURI =
T.takeWhile (/= '/') >>> T.all (/= ':')
v :: Text -> AttributeValue
v = toValue
withClass :: String -> Html -> Html
withClass className = H.span ! A.class_ (fromString className)
partitionChildren ::
[ChildDeclaration] ->
([ChildDeclaration], [ChildDeclaration], [ChildDeclaration])
partitionChildren =
reverseAll . foldl go ([], [], [])
where
go (instances, dctors, members) rcd =
case cdeclInfo rcd of
ChildInstance _ _ -> (rcd : instances, dctors, members)
ChildDataConstructor _ -> (instances, rcd : dctors, members)
ChildTypeClassMember _ -> (instances, dctors, rcd : members)
reverseAll (xs, ys, zs) = (reverse xs, reverse ys, reverse zs)