git.iko.soy/rowtype-yoga/purescript-native
on golang help sign in
Add modules for rendering HTML documentation

Refs #2520. This code is more or less copied from Pursuit, although I managed to drop the `hxt` dependency by instead using Cheapskate's provided functions for walking a rendered Markdown document. This is just a starting point towards generating HTML documentation from a given package set. Next, I plan to make psc-docs capable of producing HTML documentation as well as Markdown.

commitb03f4cfdf59751261563dedb0ad3ee83406ceb88 authorHarry Garrood <harry@garrood.me> dateSat 21 Jan 2017, 16:25 (10 years ago) parent5539c93
3 files changed+386−1
purescript.cabal+3 src/Language/PureScript/Docs/AsHtml.hs+299 src/Language/PureScript/Docs/Types.hs+84−1
purescript.cabal+3
@@ -117,9 +117,11 @@ library
117117 aeson-better-errors >= 0.8,
118118 ansi-terminal >= 0.6.2 && < 0.7,
119119 base-compat >=0.6.0,
120+ blaze-html >= 0.8.1 && < 0.9,
120121 bower-json >= 1.0.0.1 && < 1.1,
121122 boxes >= 0.1.4 && < 0.2.0,
122123 bytestring -any,
124+ cheapskate >= 0.1 && < 0.2,
123125 containers -any,
124126 clock -any,
125127 data-ordlist >= 0.4.7.0,
@@ -270,6 +272,7 @@ library
270272 Language.PureScript.Docs.RenderedCode.RenderType
271273 Language.PureScript.Docs.RenderedCode.RenderKind
272274 Language.PureScript.Docs.AsMarkdown
275+ Language.PureScript.Docs.AsHtml
273276 Language.PureScript.Docs.ParseInPackage
274277 Language.PureScript.Docs.Utils.MonoidExtras
275278
src/Language/PureScript/Docs/AsHtml.hsnew file+299
@@ -0,0 +1,299 @@
1+
2+-- | Functions for rendering generated documentation from PureScript code as
3+-- HTML.
4+
5+module Language.PureScript.Docs.AsHtml (
6+ HtmlOutput(..),
7+ HtmlOutputModule(..),
8+ HtmlRenderContext(..),
9+ nullRenderContext,
10+ declNamespace,
11+ packageAsHtml,
12+ moduleAsHtml,
13+ makeFragment,
14+ renderMarkdown
15+) where
16+
17+import Prelude
18+import Control.Arrow (second)
19+import Control.Category ((>>>))
20+import Control.Monad (unless)
21+import Data.Char (isUpper)
22+import Data.Monoid ((<>))
23+import Data.Foldable (for_)
24+import Data.String (fromString)
25+
26+import Data.Text (Text)
27+import qualified Data.Text as T
28+
29+import Text.Blaze.Html5 as H hiding (map)
30+import qualified Text.Blaze.Html5.Attributes as A
31+import qualified Cheapskate
32+
33+import qualified Language.PureScript as P
34+
35+import Language.PureScript.Docs.Types
36+import Language.PureScript.Docs.RenderedCode hiding (sp)
37+import qualified Language.PureScript.Docs.Render as Render
38+
39+declNamespace :: Declaration -> Namespace
40+declNamespace = declInfoNamespace . declInfo
41+
42+data HtmlOutput a = HtmlOutput
43+ { htmlIndex :: [(Maybe Char, a)]
44+ , htmlModules :: [(P.ModuleName, HtmlOutputModule a)]
45+ }
46+ deriving (Show, Functor)
47+
48+data HtmlOutputModule a = HtmlOutputModule
49+ { htmlOutputModuleLocals :: a
50+ , htmlOutputModuleReExports :: [(InPackage P.ModuleName, a)]
51+ }
52+ deriving (Show, Functor)
53+
54+data HtmlRenderContext = HtmlRenderContext
55+ { currentModuleName :: P.ModuleName
56+ , buildDocLink :: Namespace -> Text -> ContainingModule -> Maybe DocLink
57+ , renderDocLink :: DocLink -> Text
58+ , renderSourceLink :: P.SourceSpan -> Text
59+ }
60+
61+-- |
62+-- An HtmlRenderContext for when you don't want to render any links.
63+nullRenderContext :: P.ModuleName -> HtmlRenderContext
64+nullRenderContext mn = HtmlRenderContext
65+ { currentModuleName = mn
66+ , buildDocLink = const (const (const Nothing))
67+ , renderDocLink = const ""
68+ , renderSourceLink = const ""
69+ }
70+
71+packageAsHtml :: (P.ModuleName -> HtmlRenderContext) -> Package a -> HtmlOutput Html
72+packageAsHtml getHtmlCtx Package{..} =
73+ HtmlOutput indexFile modules
74+ where
75+ indexFile = []
76+ modules = map (\m -> moduleAsHtml (getHtmlCtx (modName m)) m) pkgModules
77+
78+moduleAsHtml :: HtmlRenderContext -> Module -> (P.ModuleName, HtmlOutputModule Html)
79+moduleAsHtml r Module{..} = (modName, HtmlOutputModule modHtml reexports)
80+ where
81+ renderDecl = declAsHtml r
82+ modHtml = do
83+ for_ modComments renderMarkdown
84+ for_ modDeclarations renderDecl
85+ reexports =
86+ map (second (foldMap renderDecl)) modReExports
87+
88+-- renderIndex :: LinksContext -> [(Maybe Char, Html)]
89+-- renderIndex LinksContext{..} = go ctxBookmarks
90+-- where
91+-- go = takeLocals
92+-- >>> groupIndex getIndex renderEntry
93+-- >>> map (second (ul . mconcat))
94+--
95+-- getIndex (_, title_) = do
96+-- c <- textHeadMay title_
97+-- guard (toUpper c `elem` ['A'..'Z'])
98+-- pure c
99+--
100+-- textHeadMay t =
101+-- case T.length t of
102+-- 0 -> Nothing
103+-- _ -> Just (T.index t 0)
104+--
105+-- renderEntry (mn, title_) =
106+-- li $ do
107+-- let url = T.pack (filePathFor mn `relativeTo` "index") <> "#" <> title_
108+-- code $
109+-- a ! A.href (v url) $ text title_
110+-- sp
111+-- text ("(" <> P.runModuleName mn <> ")")
112+--
113+-- groupIndex :: Ord i => (a -> Maybe i) -> (a -> b) -> [a] -> [(Maybe i, [b])]
114+-- groupIndex f g =
115+-- map (second DList.toList) . M.toList . foldr go' M.empty . sortBy (comparing f)
116+-- where
117+-- go' x = insertOrAppend (f x) (g x)
118+-- insertOrAppend idx val m =
119+-- let cur = M.findWithDefault DList.empty idx m
120+-- new = DList.snoc cur val
121+-- in M.insert idx new m
122+
123+declAsHtml :: HtmlRenderContext -> Declaration -> Html
124+declAsHtml r d@Declaration{..} = do
125+ let declFragment = makeFragment (declInfoNamespace declInfo) declTitle
126+ H.div ! A.class_ "decl" ! A.id (v (T.drop 1 declFragment)) $ do
127+ h3 ! A.class_ "decl__title clearfix" $ do
128+ a ! A.class_ "decl__anchor" ! A.href (v declFragment) $ "#"
129+ text declTitle
130+ for_ declSourceSpan (linkToSource r)
131+
132+ H.div ! A.class_ "decl__body" $ do
133+ case declInfo of
134+ AliasDeclaration fixity alias_ ->
135+ renderAlias fixity alias_
136+ _ ->
137+ pre ! A.class_ "decl__signature" $ code $
138+ codeAsHtml r (Render.renderDeclaration d)
139+
140+ for_ declComments renderMarkdown
141+
142+ let (instances, dctors, members) = partitionChildren declChildren
143+
144+ unless (null dctors) $ do
145+ h4 "Constructors"
146+ renderChildren r dctors
147+
148+ unless (null members) $ do
149+ h4 "Members"
150+ renderChildren r members
151+
152+ unless (null instances) $ do
153+ h4 "Instances"
154+ renderChildren r instances
155+ where
156+ linkToSource :: HtmlRenderContext -> P.SourceSpan -> Html
157+ linkToSource ctx srcspan =
158+ H.span ! A.class_ "decl__source" $
159+ a ! A.href (v (renderSourceLink ctx srcspan)) $ text "Source"
160+
161+renderChildren :: HtmlRenderContext -> [ChildDeclaration] -> Html
162+renderChildren _ [] = return ()
163+renderChildren r xs = ul $ mapM_ go xs
164+ where
165+ go decl = item decl . code . codeAsHtml r . Render.renderChildDeclaration $ decl
166+ item decl = let fragment = makeFragment (childDeclInfoNamespace (cdeclInfo decl)) (cdeclTitle decl)
167+ in li ! A.id (v (T.drop 1 fragment))
168+
169+codeAsHtml :: HtmlRenderContext -> RenderedCode -> Html
170+codeAsHtml r = outputWith elemAsHtml
171+ where
172+ elemAsHtml e = case e of
173+ Syntax x ->
174+ withClass "syntax" (text x)
175+ Keyword x ->
176+ withClass "keyword" (text x)
177+ Space ->
178+ text " "
179+ Symbol ns name link_ ->
180+ case link_ of
181+ Link mn ->
182+ let
183+ class_ = if startsWithUpper name then "ctor" else "ident"
184+ in
185+ linkToDecl ns name mn (withClass class_ (text name))
186+ NoLink ->
187+ text name
188+
189+ linkToDecl = linkToDeclaration r
190+
191+ startsWithUpper :: Text -> Bool
192+ startsWithUpper str =
193+ if T.null str
194+ then False
195+ else isUpper (T.index str 0)
196+
197+renderLink :: HtmlRenderContext -> DocLink -> Html -> Html
198+renderLink r link_@DocLink{..} =
199+ a ! A.href (v (renderDocLink r link_ <> fragmentFor link_))
200+ ! A.title (v fullyQualifiedName)
201+ where
202+ fullyQualifiedName = case linkLocation of
203+ SameModule -> fq (currentModuleName r) linkTitle
204+ LocalModule _ modName -> fq modName linkTitle
205+ DepsModule _ _ _ modName -> fq modName linkTitle
206+ BuiltinModule modName -> fq modName linkTitle
207+
208+ fq mn str = P.runModuleName mn <> "." <> str
209+
210+makeFragment :: Namespace -> Text -> Text
211+makeFragment ns = (prefix <>) . escape
212+ where
213+ prefix = case ns of
214+ TypeLevel -> "#t:"
215+ ValueLevel -> "#v:"
216+ KindLevel -> "#k:"
217+
218+ -- TODO
219+ escape = id
220+
221+fragmentFor :: DocLink -> Text
222+fragmentFor l = makeFragment (linkNamespace l) (linkTitle l)
223+
224+linkToDeclaration ::
225+ HtmlRenderContext ->
226+ Namespace ->
227+ Text ->
228+ ContainingModule ->
229+ Html ->
230+ Html
231+linkToDeclaration r ns target containMn =
232+ maybe id (renderLink r) (buildDocLink r ns target containMn)
233+
234+renderAlias :: P.Fixity -> FixityAlias -> Html
235+renderAlias (P.Fixity associativity precedence) alias_ =
236+ p $ do
237+ -- TODO: Render a link
238+ toHtml $ "Operator alias for " <> P.showQualified showAliasName alias_ <> " "
239+ em $
240+ text ("(" <> associativityStr <> " / precedence " <> T.pack (show precedence) <> ")")
241+ where
242+ showAliasName (Left valueAlias) = P.runProperName valueAlias
243+ showAliasName (Right typeAlias) = case typeAlias of
244+ (Left identifier) -> P.runIdent identifier
245+ (Right properName) -> P.runProperName properName
246+ associativityStr = case associativity of
247+ P.Infixl -> "left-associative"
248+ P.Infixr -> "right-associative"
249+ P.Infix -> "non-associative"
250+
251+-- | Render Markdown to HTML. Safe for untrusted input. Relative links are
252+-- | removed.
253+renderMarkdown :: Text -> H.Html
254+renderMarkdown =
255+ H.toMarkup . removeRelativeLinks . Cheapskate.markdown opts
256+ where
257+ opts = Cheapskate.def { Cheapskate.allowRawHtml = False }
258+
259+removeRelativeLinks :: Cheapskate.Doc -> Cheapskate.Doc
260+removeRelativeLinks = Cheapskate.walk go
261+ where
262+ go :: Cheapskate.Inlines -> Cheapskate.Inlines
263+ go = (>>= stripRelatives)
264+
265+ stripRelatives :: Cheapskate.Inline -> Cheapskate.Inlines
266+ stripRelatives (Cheapskate.Link contents_ href _)
267+ | isRelativeURI href = contents_
268+ stripRelatives other = pure other
269+
270+ -- Tests for a ':' character in the first segment of a URI.
271+ --
272+ -- See Section 4.2 of RFC 3986:
273+ -- https://tools.ietf.org/html/rfc3986#section-4.2
274+ --
275+ -- >>> isRelativeURI "http://example.com/" == False
276+ -- >>> isRelativeURI "mailto:me@example.com" == False
277+ -- >>> isRelativeURI "foo/bar" == True
278+ -- >>> isRelativeURI "/bar" == True
279+ -- >>> isRelativeURI "./bar" == True
280+ isRelativeURI :: Text -> Bool
281+ isRelativeURI =
282+ T.takeWhile (/= '/') >>> T.all (/= ':')
283+
284+v :: Text -> AttributeValue
285+v = toValue
286+
287+withClass :: String -> Html -> Html
288+withClass className content = H.span ! A.class_ (fromString className) $ content
289+
290+partitionChildren ::
291+ [ChildDeclaration] ->
292+ ([ChildDeclaration], [ChildDeclaration], [ChildDeclaration])
293+partitionChildren = foldl go ([], [], [])
294+ where
295+ go (instances, dctors, members) rcd =
296+ case cdeclInfo rcd of
297+ ChildInstance _ _ -> (rcd : instances, dctors, members)
298+ ChildDataConstructor _ -> (instances, rcd : dctors, members)
299+ ChildTypeClassMember _ -> (instances, dctors, rcd : members)
src/Language/PureScript/Docs/Types.hs+84−1
@@ -5,7 +5,7 @@ module Language.PureScript.Docs.Types
55 where
66
77import Protolude hiding (to, from)
8−import Prelude (String, unlines)
8+import Prelude (String, unlines, lookup)
99
1010import Control.Arrow ((***))
1111
@@ -353,6 +353,89 @@ ignorePackage :: InPackage a -> a
353353ignorePackage (Local x) = x
354354ignorePackage (FromDep _ x) = x
355355
356+----------------------------------------------------
357+-- Types for links between declarations
358+
359+data LinksContext = LinksContext
360+ { ctxGithub :: (GithubUser, GithubRepo)
361+ , ctxModuleMap :: Map P.ModuleName PackageName
362+ , ctxResolvedDependencies :: [(PackageName, Version)]
363+ , ctxPackageName :: PackageName
364+ , ctxVersion :: Version
365+ , ctxVersionTag :: Text
366+ }
367+ deriving (Show, Eq, Ord)
368+
369+data DocLink = DocLink
370+ { linkLocation :: LinkLocation
371+ , linkTitle :: Text
372+ , linkNamespace :: Namespace
373+ }
374+ deriving (Show, Eq, Ord)
375+
376+data LinkLocation
377+ -- | A link to a declaration in the same module.
378+ = SameModule
379+
380+ -- | A link to a declaration in a different module, but still in the current
381+ -- package; we need to store the current module and the other declaration's
382+ -- module.
383+ | LocalModule P.ModuleName P.ModuleName
384+
385+ -- | A link to a declaration in a different package. We store: current module
386+ -- name, name of the other package, version of the other package, and name of
387+ -- the module in the other package that the declaration is in.
388+ | DepsModule P.ModuleName PackageName Version P.ModuleName
389+
390+ -- | A link to a declaration that is built in to the compiler, e.g. the Prim
391+ -- module. In this case we only need to store the module that the builtin
392+ -- comes from (at the time of writing, this will only ever be "Prim").
393+ | BuiltinModule P.ModuleName
394+ deriving (Show, Eq, Ord)
395+
396+-- | Given a links context, a thing to link to (either a value or a type), and
397+-- its containing module, attempt to create a DocLink.
398+getLink :: LinksContext -> P.ModuleName -> Namespace -> Text -> ContainingModule -> Maybe DocLink
399+getLink LinksContext{..} curMn namespace target containingMod = do
400+ location <- getLinkLocation
401+ return DocLink
402+ { linkLocation = location
403+ , linkTitle = target
404+ , linkNamespace = namespace
405+ }
406+
407+ where
408+ getLinkLocation = normalLinkLocation <|> builtinLinkLocation
409+
410+ normalLinkLocation = do
411+ case containingMod of
412+ ThisModule ->
413+ return SameModule
414+ OtherModule destMn ->
415+ case Map.lookup destMn ctxModuleMap of
416+ Nothing ->
417+ return $ LocalModule curMn destMn
418+ Just pkgName -> do
419+ pkgVersion <- lookup pkgName ctxResolvedDependencies
420+ return $ DepsModule curMn pkgName pkgVersion destMn
421+
422+ builtinLinkLocation = do
423+ let primMn = P.moduleNameFromString "Prim"
424+ guard $ containingMod == OtherModule primMn
425+ -- TODO: ensure the declaration exists in the builtin module too
426+ return $ BuiltinModule primMn
427+
428+getLinksContext :: Package a -> LinksContext
429+getLinksContext Package{..} =
430+ LinksContext
431+ { ctxGithub = pkgGithub
432+ , ctxModuleMap = pkgModuleMap
433+ , ctxResolvedDependencies = pkgResolvedDependencies
434+ , ctxPackageName = bowerName pkgMeta
435+ , ctxVersion = pkgVersion
436+ , ctxVersionTag = pkgVersionTag
437+ }
438+
356439----------------------
357440-- Parsing
358441