Merge pull request #70 from leanprover/setup-single

Multi stage builds
main
Henrik Böving 2022-07-21 19:17:26 +02:00 committed by GitHub
commit 55f0399ed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 396 additions and 224 deletions

View File

@ -208,14 +208,15 @@ open Lean
open LeanInk.Annotation.Alectryon open LeanInk.Annotation.Alectryon
open scoped DocGen4.Jsx open scoped DocGen4.Jsx
def moduleToHtml (module : Process.Module) (inkPath : System.FilePath) (sourceFilePath : System.FilePath) : HtmlT IO Html := withReader (setCurrentName module.name) do def moduleToHtml (module : Process.Module) (inkPath : System.FilePath) (sourceFilePath : System.FilePath) : HtmlT IO Html := withTheReader SiteBaseContext (setCurrentName module.name) do
let json ← runInk inkPath sourceFilePath let json ← runInk inkPath sourceFilePath
let fragments := fromJson? json let fragments := fromJson? json
match fragments with match fragments with
| .ok fragments => | .ok fragments =>
let render := StateT.run (LeanInk.Annotation.Alectryon.renderFragments fragments) { counter := 0 } let render := StateT.run (LeanInk.Annotation.Alectryon.renderFragments fragments) { counter := 0 }
let ctx ← read let ctx ← read
let (html, _) := ReaderT.run render ctx let baseCtx ← readThe SiteBaseContext
let (html, _) := render |>.run ctx baseCtx
pure html pure html
| .error err => throw $ IO.userError s!"Error while parsing LeanInk Output: {err}" | .error err => throw $ IO.userError s!"Error while parsing LeanInk Output: {err}"

View File

@ -19,7 +19,7 @@ Sets up a lake workspace for the current project. Furthermore initialize
the Lean search path with the path to the proper compiler from lean-toolchain the Lean search path with the path to the proper compiler from lean-toolchain
as well as all the dependencies. as well as all the dependencies.
-/ -/
def lakeSetup (imports : List String) : IO (Except UInt32 (Lake.Workspace × String)) := do def lakeSetup (imports : List String) : IO (Except UInt32 Lake.Workspace) := do
let (leanInstall?, lakeInstall?) ← Lake.findInstall? let (leanInstall?, lakeInstall?) ← Lake.findInstall?
match ←(EIO.toIO' $ Lake.mkLoadConfig {leanInstall?, lakeInstall?}) with match ←(EIO.toIO' $ Lake.mkLoadConfig {leanInstall?, lakeInstall?}) with
| .ok config => | .ok config =>
@ -30,16 +30,23 @@ def lakeSetup (imports : List String) : IO (Except UInt32 (Lake.Workspace × Str
let ctx ← Lake.mkBuildContext ws let ctx ← Lake.mkBuildContext ws
(ws.root.buildImportsAndDeps imports *> pure ()) |>.run Lake.MonadLog.eio ctx (ws.root.buildImportsAndDeps imports *> pure ()) |>.run Lake.MonadLog.eio ctx
initSearchPath (←findSysroot) ws.leanPaths.oleanPath initSearchPath (←findSysroot) ws.leanPaths.oleanPath
pure $ Except.ok (ws, libraryLeanGitHash) pure $ Except.ok ws
| .error err => | .error err =>
throw $ IO.userError err.toString throw $ IO.userError err.toString
def envOfImports (imports : List Name) : IO Environment := do
importModules (imports.map (Import.mk · false)) Options.empty
def loadInit (imports : List Name) : IO Hierarchy := do
let env ← envOfImports imports
pure $ Hierarchy.fromArray env.header.moduleNames
/-- /--
Load a list of modules from the current Lean search path into an `Environment` Load a list of modules from the current Lean search path into an `Environment`
to process for documentation. to process for documentation.
-/ -/
def load (imports : List Name) : IO Process.AnalyzerResult := do def load (task : Process.AnalyzeTask) : IO (Process.AnalyzerResult × Hierarchy) := do
let env ← importModules (List.map (Import.mk · false) imports) Options.empty let env ← envOfImports task.getLoad
IO.println "Processing modules" IO.println "Processing modules"
let config := { let config := {
-- TODO: parameterize maxHeartbeats -- TODO: parameterize maxHeartbeats
@ -49,6 +56,7 @@ def load (imports : List Name) : IO Process.AnalyzerResult := do
fileName := default, fileName := default,
fileMap := default, fileMap := default,
} }
Prod.fst <$> Meta.MetaM.toIO Process.process config { env := env} {} {}
Prod.fst <$> Meta.MetaM.toIO (Process.process task) config { env := env } {} {}
end DocGen4 end DocGen4

View File

@ -11,86 +11,103 @@ import DocGen4.Output.Index
import DocGen4.Output.Module import DocGen4.Output.Module
import DocGen4.Output.NotFound import DocGen4.Output.NotFound
import DocGen4.Output.Find import DocGen4.Output.Find
import DocGen4.Output.Semantic
import DocGen4.Output.SourceLinker import DocGen4.Output.SourceLinker
import DocGen4.LeanInk.Output import DocGen4.LeanInk.Output
import Std.Data.HashMap
namespace DocGen4 namespace DocGen4
open Lean IO System Output Process open Lean IO System Output Process Std
/-- def basePath := FilePath.mk "." / "build" / "doc"
The main entrypoint for outputting the documentation HTML based on an def srcBasePath := basePath / "src"
`AnalyzerResult`.
-/ def htmlOutputSetup (config : SiteBaseContext) : IO Unit := do
def htmlOutput (result : AnalyzerResult) (ws : Lake.Workspace) (leanHash: String) (inkPath : Option System.FilePath) : IO Unit := do let findBasePath := basePath / "find"
let config := {
depthToRoot := 0, -- Base structure
FS.createDirAll basePath
FS.createDirAll (basePath / "find")
FS.createDirAll srcBasePath
-- All the doc-gen static stuff
let indexHtml := ReaderT.run index config |>.toString
let notFoundHtml := ReaderT.run notFound config |>.toString
let docGenStatic := #[
("style.css", styleCss),
("declaration-data.js", declarationDataCenterJs),
("nav.js", navJs),
("how-about.js", howAboutJs),
("search.js", searchJs),
("mathjax-config.js", mathjaxConfigJs),
("index.html", indexHtml),
("404.html", notFoundHtml)
]
for (fileName, content) in docGenStatic do
FS.writeFile (basePath / fileName) content
let findHtml := ReaderT.run find { config with depthToRoot := 1 } |>.toString
let findStatic := #[
("index.html", findHtml),
("find.js", findJs)
]
for (fileName, content) in findStatic do
FS.writeFile (findBasePath / fileName) content
let alectryonStatic := #[
("alectryon.css", alectryonCss),
("alectryon.js", alectryonJs),
("docutils_basic.css", docUtilsCss),
("pygments.css", pygmentsCss)
]
for (fileName, content) in alectryonStatic do
FS.writeFile (srcBasePath / fileName) content
def DocInfo.toJson (module : Name) (info : DocInfo) : HtmlM Json := do
let name := info.getName.toString
let doc := info.getDocString.getD ""
let docLink ← declNameToLink info.getName
let sourceLink ← getSourceUrl module info.getDeclarationRange
pure $ Json.mkObj [("name", name), ("doc", doc), ("docLink", docLink), ("sourceLink", sourceLink)]
def Process.Module.toJson (module : Module) : HtmlM (Array Json) := do
let mut jsonDecls := #[]
for decl in filterMapDocInfo module.members do
let json ← DocInfo.toJson module.name decl
jsonDecls := jsonDecls.push json
pure jsonDecls
def htmlOutputResults (baseConfig : SiteBaseContext) (result : AnalyzerResult) (ws : Lake.Workspace) (inkPath : Option System.FilePath) : IO Unit := do
let config : SiteContext := {
result := result, result := result,
currentName := none, sourceLinker := ←sourceLinker ws
sourceLinker := ←sourceLinker ws leanHash
leanInkEnabled := inkPath.isSome leanInkEnabled := inkPath.isSome
} }
let basePath := FilePath.mk "." / "build" / "doc"
let srcBasePath := basePath / "src" FS.createDirAll basePath
let indexHtml := ReaderT.run index config
let findHtml := ReaderT.run find { config with depthToRoot := 1 }
let notFoundHtml := ReaderT.run notFound config
-- Rendering the entire lean compiler takes time.... -- Rendering the entire lean compiler takes time....
--let sourceSearchPath := ((←Lean.findSysroot) / "src" / "lean") :: ws.root.srcDir :: ws.leanSrcPath --let sourceSearchPath := ((←Lean.findSysroot) / "src" / "lean") :: ws.root.srcDir :: ws.leanSrcPath
let sourceSearchPath := ws.root.srcDir :: ws.leanSrcPath let sourceSearchPath := ws.root.srcDir :: ws.leanSrcPath
FS.createDirAll basePath let mut declMap := HashMap.empty
FS.createDirAll (basePath / "find")
FS.createDirAll (basePath / "semantic")
FS.createDirAll srcBasePath
let mut declList := #[]
for (_, mod) in result.moduleInfo.toArray do for (_, mod) in result.moduleInfo.toArray do
for decl in filterMapDocInfo mod.members do let topLevelMod := mod.name.getRoot
let name := decl.getName.toString let jsonDecls := Module.toJson mod |>.run config baseConfig
let config := { config with depthToRoot := 0 } let currentModDecls := declMap.findD topLevelMod #[]
let doc := decl.getDocString.getD "" declMap := declMap.insert topLevelMod (currentModDecls ++ jsonDecls)
let root := Id.run <| ReaderT.run (getRoot) config
let link := root ++ s!"../semantic/{decl.getName.hash}.xml#"
let docLink := Id.run <| ReaderT.run (declNameToLink decl.getName) config
let sourceLink := Id.run <| ReaderT.run (getSourceUrl mod.name decl.getDeclarationRange) config
let obj := Json.mkObj [("name", name), ("doc", doc), ("link", link), ("docLink", docLink), ("sourceLink", sourceLink)]
declList := declList.push obj
let xml := toString <| Id.run <| ReaderT.run (semanticXml decl) config
FS.writeFile (basePath / "semantic" / s!"{decl.getName.hash}.xml") xml
let json := Json.arr declList
FS.writeFile (basePath / "semantic" / "docgen4.xml") <| toString <| Id.run <| ReaderT.run schemaXml config for (topLevelMod, decls) in declMap.toList do
FS.writeFile (basePath / s!"declaration-data-{topLevelMod}.bmp") (Json.arr decls).compress
FS.writeFile (basePath / "index.html") indexHtml.toString
FS.writeFile (basePath / "404.html") notFoundHtml.toString
FS.writeFile (basePath / "find" / "index.html") findHtml.toString
FS.writeFile (basePath / "style.css") styleCss
let declarationDataPath := basePath / "declaration-data.bmp"
FS.writeFile declarationDataPath json.compress
FS.writeFile (basePath / "declaration-data.timestamp") <| toString (←declarationDataPath.metadata).modified.sec
FS.writeFile (basePath / "declaration-data.js") declarationDataCenterJs
FS.writeFile (basePath / "nav.js") navJs
FS.writeFile (basePath / "find" / "find.js") findJs
FS.writeFile (basePath / "how-about.js") howAboutJs
FS.writeFile (basePath / "search.js") searchJs
FS.writeFile (basePath / "mathjax-config.js") mathjaxConfigJs
FS.writeFile (srcBasePath / "alectryon.css") alectryonCss
FS.writeFile (srcBasePath / "alectryon.js") alectryonJs
FS.writeFile (srcBasePath / "docutils_basic.css") docUtilsCss
FS.writeFile (srcBasePath / "pygments.css") pygmentsCss
for (modName, module) in result.moduleInfo.toArray do for (modName, module) in result.moduleInfo.toArray do
let fileDir := moduleNameToDirectory basePath modName let fileDir := moduleNameToDirectory basePath modName
let filePath := moduleNameToFile basePath modName let filePath := moduleNameToFile basePath modName
-- path: 'basePath/module/components/till/last.html' -- path: 'basePath/module/components/till/last.html'
-- The last component is the file name, so we drop it from the depth to root. -- The last component is the file name, so we drop it from the depth to root.
let config := { config with depthToRoot := modName.components.dropLast.length } let baseConfig := { baseConfig with depthToRoot := modName.components.dropLast.length }
let moduleHtml := ReaderT.run (moduleToHtml module) config let moduleHtml := moduleToHtml module |>.run config baseConfig
FS.createDirAll $ fileDir FS.createDirAll $ fileDir
FS.writeFile filePath moduleHtml.toString FS.writeFile filePath moduleHtml.toString
if let some inkPath := inkPath then if let some inkPath := inkPath then
@ -98,12 +115,40 @@ def htmlOutput (result : AnalyzerResult) (ws : Lake.Workspace) (leanHash: String
IO.println s!"Inking: {modName.toString}" IO.println s!"Inking: {modName.toString}"
-- path: 'basePath/src/module/components/till/last.html' -- path: 'basePath/src/module/components/till/last.html'
-- The last component is the file name, however we are in src/ here so dont drop it this time -- The last component is the file name, however we are in src/ here so dont drop it this time
let config := { config with depthToRoot := modName.components.length } let baseConfig := { baseConfig with depthToRoot := modName.components.length }
let srcHtml ← ReaderT.run (LeanInk.moduleToHtml module inkPath inputPath) config let srcHtml ← LeanInk.moduleToHtml module inkPath inputPath |>.run config baseConfig
let srcDir := moduleNameToDirectory srcBasePath modName let srcDir := moduleNameToDirectory srcBasePath modName
let srcPath := moduleNameToFile srcBasePath modName let srcPath := moduleNameToFile srcBasePath modName
FS.createDirAll srcDir FS.createDirAll srcDir
FS.writeFile srcPath srcHtml.toString FS.writeFile srcPath srcHtml.toString
def getSimpleBaseContext (hierarchy : Hierarchy) : SiteBaseContext :=
{
depthToRoot := 0,
currentName := none,
hierarchy
}
def htmlOutputFinalize (baseConfig : SiteBaseContext) : IO Unit := do
htmlOutputSetup baseConfig
let mut topLevelModules := #[]
for entry in ←System.FilePath.readDir basePath do
if entry.fileName.startsWith "declaration-data-" && entry.fileName.endsWith ".bmp" then
let module := entry.fileName.drop "declaration-data-".length |>.dropRight ".bmp".length
topLevelModules := topLevelModules.push (Json.str module)
-- The root JSON for find
FS.writeFile (basePath / "declaration-data.bmp") (Json.arr topLevelModules).compress
/--
The main entrypoint for outputting the documentation HTML based on an
`AnalyzerResult`.
-/
def htmlOutput (result : AnalyzerResult) (hierarchy : Hierarchy) (ws : Lake.Workspace) (inkPath : Option System.FilePath) : IO Unit := do
let baseConfig := getSimpleBaseContext hierarchy
htmlOutputResults baseConfig result ws inkPath
htmlOutputFinalize baseConfig
end DocGen4 end DocGen4

View File

@ -13,13 +13,14 @@ open scoped DocGen4.Jsx
open Lean System Widget Elab Process open Lean System Widget Elab Process
/-- /--
The context used in the `HtmlM` monad for HTML templating. The context used in the `BaseHtmlM` monad for HTML templating.
-/ -/
structure SiteContext where structure SiteBaseContext where
/-- /--
The full analysis result from the Process module. The module hierarchy as a tree structure.
-/ -/
result : AnalyzerResult hierarchy : Hierarchy
/-- /--
How far away we are from the page root, used for relative links to the root. How far away we are from the page root, used for relative links to the root.
-/ -/
@ -29,6 +30,15 @@ structure SiteContext where
pages that don't have a module name. pages that don't have a module name.
-/ -/
currentName : Option Name currentName : Option Name
/--
The context used in the `HtmlM` monad for HTML templating.
-/
structure SiteContext where
/--
The full analysis result from the Process module.
-/
result : AnalyzerResult
/-- /--
A function to link declaration names to their source URLs, usually Github ones. A function to link declaration names to their source URLs, usually Github ones.
-/ -/
@ -38,50 +48,63 @@ structure SiteContext where
-/ -/
leanInkEnabled : Bool leanInkEnabled : Bool
def setCurrentName (name : Name) (ctx : SiteContext) := {ctx with currentName := some name} def setCurrentName (name : Name) (ctx : SiteBaseContext) := {ctx with currentName := some name}
abbrev HtmlT := ReaderT SiteContext abbrev BaseHtmlT := ReaderT SiteBaseContext
abbrev BaseHtmlM := BaseHtmlT Id
abbrev HtmlT (m) := ReaderT SiteContext (BaseHtmlT m)
abbrev HtmlM := HtmlT Id abbrev HtmlM := HtmlT Id
def HtmlT.run (x : HtmlT m α) (ctx : SiteContext) (baseCtx : SiteBaseContext) : m α :=
ReaderT.run x ctx |>.run baseCtx
def HtmlM.run (x : HtmlM α) (ctx : SiteContext) (baseCtx : SiteBaseContext) : α :=
ReaderT.run x ctx |>.run baseCtx |>.run
/-- /--
Obtains the root URL as a relative one to the current depth. Obtains the root URL as a relative one to the current depth.
-/ -/
def getRoot : HtmlM String := do def getRoot : BaseHtmlM String := do
let rec go: Nat -> String let rec go: Nat -> String
| 0 => "./" | 0 => "./"
| Nat.succ n' => "../" ++ go n' | Nat.succ n' => "../" ++ go n'
let d <- SiteContext.depthToRoot <$> read let d <- SiteBaseContext.depthToRoot <$> read
return (go d) return (go d)
def getHierarchy : BaseHtmlM Hierarchy := do pure (←read).hierarchy
def getCurrentName : BaseHtmlM (Option Name) := do pure (←read).currentName
def getResult : HtmlM AnalyzerResult := do pure (←read).result def getResult : HtmlM AnalyzerResult := do pure (←read).result
def getCurrentName : HtmlM (Option Name) := do pure (←read).currentName
def getSourceUrl (module : Name) (range : Option DeclarationRange): HtmlM String := do pure $ (←read).sourceLinker module range def getSourceUrl (module : Name) (range : Option DeclarationRange): HtmlM String := do pure $ (←read).sourceLinker module range
def leanInkEnabled? : HtmlM Bool := do pure (←read).leanInkEnabled def leanInkEnabled? : HtmlM Bool := do pure (←read).leanInkEnabled
/-- /--
If a template is meant to be extended because it for example only provides the If a template is meant to be extended because it for example only provides the
header but no real content this is the way to fill the template with content. header but no real content this is the way to fill the template with content.
This is untyped so HtmlM and BaseHtmlM can be mixed.
-/ -/
def templateExtends {α β : Type} (base : α → HtmlM β) (new : HtmlM α) : HtmlM β := def templateExtends {α β} {m} [Bind m] (base : α → m β) (new : m α) : m β :=
new >>= base new >>= base
def templateLiftExtends {α β} {m n} [Bind m] [MonadLift n m] (base : α → n β) (new : m α) : m β :=
new >>= (monadLift ∘ base)
/-- /--
Returns the doc-gen4 link to a module name. Returns the doc-gen4 link to a module name.
-/ -/
def moduleNameToLink (n : Name) : HtmlM String := do def moduleNameToLink (n : Name) : BaseHtmlM String := do
let parts := n.components.map Name.toString let parts := n.components.map Name.toString
pure $ (← getRoot) ++ (parts.intersperse "/").foldl (· ++ ·) "" ++ ".html" pure $ (← getRoot) ++ (parts.intersperse "/").foldl (· ++ ·) "" ++ ".html"
/-- /--
Returns the HTML doc-gen4 link to a module name. Returns the HTML doc-gen4 link to a module name.
-/ -/
def moduleToHtmlLink (module : Name) : HtmlM Html := do def moduleToHtmlLink (module : Name) : BaseHtmlM Html := do
pure <a href={←moduleNameToLink module}>{module.toString}</a> pure <a href={←moduleNameToLink module}>{module.toString}</a>
/-- /--
Returns the LeanInk link to a module name. Returns the LeanInk link to a module name.
-/ -/
def moduleNameToInkLink (n : Name) : HtmlM String := do def moduleNameToInkLink (n : Name) : BaseHtmlM String := do
let parts := "src" :: n.components.map Name.toString let parts := "src" :: n.components.map Name.toString
pure $ (← getRoot) ++ (parts.intersperse "/").foldl (· ++ ·) "" ++ ".html" pure $ (← getRoot) ++ (parts.intersperse "/").foldl (· ++ ·) "" ++ ".html"

View File

@ -6,13 +6,13 @@ namespace Output
open scoped DocGen4.Jsx open scoped DocGen4.Jsx
open Lean open Lean
def find : HtmlM Html := do def find : BaseHtmlM Html := do
pure pure
<html lang="en"> <html lang="en">
<head> <head>
<link rel="preload" href={s!"{←getRoot}declaration-data.bmp"}/> <link rel="preload" href={s!"{←getRoot}declaration-data.bmp"}/>
<script>{s!"const SITE_ROOT={String.quote (←getRoot)};"}</script> <script>{s!"const SITE_ROOT={String.quote (←getRoot)};"}</script>
<script type="module" async="true" src={s!"./find.js"}></script> <script type="module" async="true" src="./find.js"></script>
</head> </head>
<body></body> <body></body>
</html> </html>

View File

@ -11,7 +11,7 @@ namespace Output
open scoped DocGen4.Jsx open scoped DocGen4.Jsx
def index : HtmlM Html := do templateExtends (baseHtml "Index") $ pure $ def index : BaseHtmlM Html := do templateExtends (baseHtml "Index") $ pure $
<main> <main>
<a id="top"></a> <a id="top"></a>
<h1> Welcome to the documentation page </h1> <h1> Welcome to the documentation page </h1>

View File

@ -233,10 +233,10 @@ def internalNav (members : Array Name) (moduleName : Name) : HtmlM Html := do
/-- /--
The main entry point to rendering the HTML for an entire module. The main entry point to rendering the HTML for an entire module.
-/ -/
def moduleToHtml (module : Process.Module) : HtmlM Html := withReader (setCurrentName module.name) do def moduleToHtml (module : Process.Module) : HtmlM Html := withTheReader SiteBaseContext (setCurrentName module.name) do
let memberDocs ← module.members.mapM (λ i => moduleMemberToHtml module.name i) let memberDocs ← module.members.mapM (λ i => moduleMemberToHtml module.name i)
let memberNames := filterMapDocInfo module.members |>.map DocInfo.getName let memberNames := filterMapDocInfo module.members |>.map DocInfo.getName
templateExtends (baseHtmlGenerator module.name.toString) $ pure #[ templateLiftExtends (baseHtmlGenerator module.name.toString) $ pure #[
←internalNav memberNames module.name, ←internalNav memberNames module.name,
Html.element "main" false #[] memberDocs Html.element "main" false #[] memberDocs
] ]

View File

@ -13,7 +13,7 @@ namespace Output
open Lean open Lean
open scoped DocGen4.Jsx open scoped DocGen4.Jsx
def moduleListFile (file : Name) : HtmlM Html := do def moduleListFile (file : Name) : BaseHtmlM Html := do
pure <div class={if (← getCurrentName) == file then "nav_link visible" else "nav_link"}> pure <div class={if (← getCurrentName) == file then "nav_link visible" else "nav_link"}>
{←moduleToHtmlLink file} {←moduleToHtmlLink file}
</div> </div>
@ -21,7 +21,7 @@ def moduleListFile (file : Name) : HtmlM Html := do
/-- /--
Build the HTML tree representing the module hierarchy. Build the HTML tree representing the module hierarchy.
-/ -/
partial def moduleListDir (h : Hierarchy) : HtmlM Html := do partial def moduleListDir (h : Hierarchy) : BaseHtmlM Html := do
let children := Array.mk (h.getChildren.toList.map Prod.snd) let children := Array.mk (h.getChildren.toList.map Prod.snd)
let dirs := children.filter (λ c => c.getChildren.toList.length != 0) let dirs := children.filter (λ c => c.getChildren.toList.length != 0)
let files := children.filter (λ c => Hierarchy.isFile c ∧ c.getChildren.toList.length = 0) let files := children.filter (λ c => Hierarchy.isFile c ∧ c.getChildren.toList.length = 0)
@ -30,7 +30,7 @@ partial def moduleListDir (h : Hierarchy) : HtmlM Html := do
let fileNodes ← (files.mapM moduleListFile) let fileNodes ← (files.mapM moduleListFile)
let moduleLink ← moduleNameToLink h.getName let moduleLink ← moduleNameToLink h.getName
let summary := let summary :=
if (←getResult).moduleInfo.contains h.getName then if h.isFile then
<summary>{←moduleToHtmlLink h.getName}</summary> <summary>{←moduleToHtmlLink h.getName}</summary>
else else
<summary>{h.getName.toString}</summary> <summary>{h.getName.toString}</summary>
@ -45,17 +45,17 @@ partial def moduleListDir (h : Hierarchy) : HtmlM Html := do
/-- /--
Return a list of top level modules, linkified and rendered as HTML Return a list of top level modules, linkified and rendered as HTML
-/ -/
def moduleList : HtmlM Html := do def moduleList : BaseHtmlM Html := do
let hierarchy := (←getResult).hierarchy let hierarchy ← getHierarchy
let mut list := Array.empty let mut list := Array.empty
for (n, cs) in hierarchy.getChildren do for (_, cs) in hierarchy.getChildren do
list := list.push $ ←moduleListDir cs list := list.push $ ←moduleListDir cs
pure <div class="module_list">[list]</div> pure <div class="module_list">[list]</div>
/-- /--
The main entry point to rendering the navbar on the left hand side. The main entry point to rendering the navbar on the left hand side.
-/ -/
def navbar : HtmlM Html := do def navbar : BaseHtmlM Html := do
pure pure
<nav class="nav"> <nav class="nav">
<h3>General documentation</h3> <h3>General documentation</h3>

View File

@ -14,7 +14,7 @@ open scoped DocGen4.Jsx
/-- /--
Render the 404 page. Render the 404 page.
-/ -/
def notFound : HtmlM Html := do templateExtends (baseHtml "404") $ pure $ def notFound : BaseHtmlM Html := do templateExtends (baseHtml "404") $ pure $
<main> <main>
<h1>404 Not Found</h1> <h1>404 Not Found</h1>
<p> Unfortunately, the page you were looking for is no longer here. </p> <p> Unfortunately, the page you were looking for is no longer here. </p>

View File

@ -1,59 +0,0 @@
import DocGen4.Output.Template
import DocGen4.Output.DocString
import DocGen4.Process
import Lean.Data.Xml
open Lean Xml DocGen4.Process
namespace DocGen4
namespace Output
instance : ToString $ Array Element where
toString xs := xs.map toString |>.foldl String.append ""
instance : Coe Element Content where
coe e := Content.Element e
-- TODO: syntax metaprogramming and basic semantic data
def semanticXml (i : DocInfo) : HtmlM $ Array Element := do
pure #[
Element.Element
"rdf:RDF"
(Std.RBMap.fromList [
("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("xmlns:docgen4", s!"{←getRoot}semactic/docgen4.xml#")
] _)
#[
Element.Element
"rdf:Description"
(Std.RBMap.fromList [
("rdf:about", s!"{←getRoot}semactic/{i.getName.hash}.xml#")
] _)
#[]
]
]
def schemaXml : HtmlM $ Array Element := do
pure #[
Element.Element
"rdf:RDF"
(Std.RBMap.fromList [
("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("xmlns:docgen4", s!"{←getRoot}semactic/docgen4.xml#")
] _)
#[
Element.Element
"docgen4:hasInstance"
Std.RBMap.empty
#[
Element.Element
"rdfs:type"
Std.RBMap.empty
#[Content.Character "rdf:Property"]
]
]
]
end Output
end DocGen4

View File

@ -54,7 +54,8 @@ Given a lake workspace with all the dependencies as well as the hash of the
compiler release to work with this provides a function to turn names of compiler release to work with this provides a function to turn names of
declarations into (optionally positional) Github URLs. declarations into (optionally positional) Github URLs.
-/ -/
def sourceLinker (ws : Lake.Workspace) (leanHash : String): IO (Name → Option DeclarationRange → String) := do def sourceLinker (ws : Lake.Workspace) : IO (Name → Option DeclarationRange → String) := do
let leanHash := ws.env.lean.githash
-- Compute a map from package names to source URL -- Compute a map from package names to source URL
let mut gitMap := Std.mkHashMap let mut gitMap := Std.mkHashMap
let projectBaseUrl := getGithubBaseUrl (←getProjectGithubUrl) let projectBaseUrl := getGithubBaseUrl (←getProjectGithubUrl)

View File

@ -14,7 +14,7 @@ open scoped DocGen4.Jsx
/-- /--
The HTML template used for all pages. The HTML template used for all pages.
-/ -/
def baseHtmlGenerator (title : String) (site : Array Html) : HtmlM Html := do def baseHtmlGenerator (title : String) (site : Array Html) : BaseHtmlM Html := do
pure pure
<html lang="en"> <html lang="en">
<head> <head>
@ -66,7 +66,7 @@ def baseHtmlGenerator (title : String) (site : Array Html) : HtmlM Html := do
/-- /--
A comfortability wrapper around `baseHtmlGenerator`. A comfortability wrapper around `baseHtmlGenerator`.
-/ -/
def baseHtml (title : String) (site : Html) : HtmlM Html := baseHtmlGenerator title #[site] def baseHtml (title : String) (site : Html) : BaseHtmlM Html := baseHtmlGenerator title #[site]
end Output end Output
end DocGen4 end DocGen4

View File

@ -6,14 +6,20 @@ Authors: Henrik Böving
import Lean import Lean
import Std.Data.HashMap import Std.Data.HashMap
import Std.Data.HashSet
import DocGen4.Process.Base import DocGen4.Process.Base
import DocGen4.Process.Hierarchy import DocGen4.Process.Hierarchy
import DocGen4.Process.DocInfo import DocGen4.Process.DocInfo
open Std
def HashSet.fromArray [BEq α] [Hashable α] (xs : Array α) : HashSet α :=
xs.foldr (flip .insert) .empty
namespace DocGen4.Process namespace DocGen4.Process
open Lean Meta Std open Lean Meta
/-- /--
Member of a module, either a declaration or some module doc string. Member of a module, either a declaration or some module doc string.
@ -54,10 +60,6 @@ structure AnalyzerResult where
-/ -/
moduleInfo : HashMap Name Module moduleInfo : HashMap Name Module
/-- /--
The module hierarchy as a tree structure.
-/
hierarchy : Hierarchy
/--
An adjacency matrix for the import relation between modules, indexed An adjacency matrix for the import relation between modules, indexed
my the values in `name2ModIdx`. my the values in `name2ModIdx`.
-/ -/
@ -86,21 +88,65 @@ def getDocString : ModuleMember → Option String
end ModuleMember end ModuleMember
def getRelevantModules (imports : List Name) : MetaM (HashSet Name) := do
let env ← getEnv
let mut relevant := .empty
for module in env.header.moduleNames do
if module.getRoot ∈ imports then
relevant := relevant.insert module
pure relevant
inductive AnalyzeTask where
| loadAll (load : List Name) : AnalyzeTask
| loadAllLimitAnalysis (load : List Name) (analyze : List Name) : AnalyzeTask
def AnalyzeTask.getLoad : AnalyzeTask → List Name
| loadAll load => load
| loadAllLimitAnalysis load _ => load
def AnalyzeTask.getAnalyze : AnalyzeTask → List Name
| loadAll load => load
| loadAllLimitAnalysis _ analysis => analysis
def getAllModuleDocs (relevantModules : Array Name) : MetaM (HashMap Name Module) := do
let env ← getEnv
let mut res := mkHashMap relevantModules.size
for module in relevantModules do
let modDocs := getModuleDoc? env module |>.getD #[] |>.map .modDoc
res := res.insert module (Module.mk module modDocs)
pure res
-- TODO: This is definitely not the most efficient way to store this data
def buildImportAdjMatrix (allModules : Array Name) : MetaM (Array (Array Bool)) := do
let env ← getEnv
let mut adj := Array.mkArray allModules.size (Array.mkArray allModules.size false)
for moduleName in allModules do
let some modIdx := env.getModuleIdx? moduleName | unreachable!
let moduleData := env.header.moduleData.get! modIdx
for imp in moduleData.imports do
let some importIdx := env.getModuleIdx? imp.module | unreachable!
adj := adj.set! modIdx (adj.get! modIdx |>.set! importIdx true)
pure adj
/-- /--
Run the doc-gen analysis on all modules that are loaded into the `Environment` Run the doc-gen analysis on all modules that are loaded into the `Environment`
of this `MetaM` run. of this `MetaM` run and mentioned by the `AnalyzeTask`.
-/ -/
def process : MetaM AnalyzerResult := do def process (task : AnalyzeTask) : MetaM (AnalyzerResult × Hierarchy) := do
let env ← getEnv let env ← getEnv
let mut res := mkHashMap env.header.moduleNames.size let relevantModules ← match task with
for module in env.header.moduleNames do | .loadAll _ => pure $ HashSet.fromArray env.header.moduleNames
let modDocs := match getModuleDoc? env module with | .loadAllLimitAnalysis _ analysis => getRelevantModules analysis
| none => #[] let allModules := env.header.moduleNames
| some ds => ds
|>.map (λ doc => ModuleMember.modDoc doc) let mut res ← getAllModuleDocs relevantModules.toArray
res := res.insert module (Module.mk module modDocs)
for (name, cinfo) in env.constants.toList do
let some modidx := env.getModuleIdxFor? name | unreachable!
let moduleName := env.allImportedModuleNames.get! modidx
if !relevantModules.contains moduleName then
continue
for cinfo in env.constants.toList do
try try
let config := { let config := {
maxHeartbeats := 5000000, maxHeartbeats := 5000000,
@ -108,33 +154,28 @@ def process : MetaM AnalyzerResult := do
fileName := ←getFileName, fileName := ←getFileName,
fileMap := ←getFileMap fileMap := ←getFileMap
} }
let analysis := Prod.fst <$> Meta.MetaM.toIO (DocInfo.ofConstant cinfo) config { env := env} {} {} let analysis := Prod.fst <$> Meta.MetaM.toIO (DocInfo.ofConstant (name, cinfo)) config { env := env } {} {}
if let some dinfo ← analysis then if let some dinfo ← analysis then
let some modidx := env.getModuleIdxFor? dinfo.getName | unreachable!
let moduleName := env.allImportedModuleNames.get! modidx let moduleName := env.allImportedModuleNames.get! modidx
let module := res.find! moduleName let module := res.find! moduleName
res := res.insert moduleName {module with members := module.members.push (ModuleMember.docInfo dinfo)} res := res.insert moduleName {module with members := module.members.push (ModuleMember.docInfo dinfo)}
catch e => catch e =>
IO.println s!"WARNING: Failed to obtain information for: {cinfo.fst}: {←e.toMessageData.toString}" IO.println s!"WARNING: Failed to obtain information for: {name}: {←e.toMessageData.toString}"
-- TODO: This is definitely not the most efficient way to store this data let adj ← buildImportAdjMatrix allModules
let mut adj := Array.mkArray res.size (Array.mkArray res.size false)
-- TODO: This could probably be faster if we did an insertion sort above instead -- TODO: This could probably be faster if we did sorted insert above instead
for (moduleName, module) in res.toArray do for (moduleName, module) in res.toArray do
res := res.insert moduleName {module with members := module.members.qsort ModuleMember.order} res := res.insert moduleName {module with members := module.members.qsort ModuleMember.order}
let some modIdx := env.getModuleIdx? moduleName | unreachable!
let moduleData := env.header.moduleData.get! modIdx
for imp in moduleData.imports do
let some importIdx := env.getModuleIdx? imp.module | unreachable!
adj := adj.set! modIdx (adj.get! modIdx |>.set! importIdx true)
pure { let hierarchy := Hierarchy.fromArray allModules
let analysis := {
name2ModIdx := env.const2ModIdx, name2ModIdx := env.const2ModIdx,
moduleNames := env.header.moduleNames, moduleNames := allModules,
moduleInfo := res, moduleInfo := res,
hierarchy := Hierarchy.fromArray env.header.moduleNames,
importAdj := adj importAdj := adj
} }
pure (analysis, hierarchy)
def filterMapDocInfo (ms : Array ModuleMember) : Array DocInfo := def filterMapDocInfo (ms : Array ModuleMember) : Array DocInfo :=
ms.filterMap filter ms.filterMap filter

View File

@ -27,6 +27,9 @@ namespace HierarchyMap
def toList : HierarchyMap → List (Name × Hierarchy) def toList : HierarchyMap → List (Name × Hierarchy)
| t => t.revFold (fun ps k v => (k, v)::ps) [] | t => t.revFold (fun ps k v => (k, v)::ps) []
def toArray : HierarchyMap → Array (Name × Hierarchy)
| t => t.fold (fun ps k v => ps ++ #[(k, v)] ) #[]
def hForIn [Monad m] (t : HierarchyMap) (init : σ) (f : (Name × Hierarchy) → σ → m (ForInStep σ)) : m σ := def hForIn [Monad m] (t : HierarchyMap) (init : σ) (f : (Name × Hierarchy) → σ → m (ForInStep σ)) : m σ :=
t.forIn init (fun a b acc => f (a, b) acc) t.forIn init (fun a b acc => f (a, b) acc)
@ -58,7 +61,7 @@ partial def insert! (h : Hierarchy) (n : Name) : Hierarchy := Id.run $ do
| none => | none =>
node hn h.isFile (cs.insert Name.cmp n $ empty n true) node hn h.isFile (cs.insert Name.cmp n $ empty n true)
| some (node _ true _) => h | some (node _ true _) => h
| some hierarchy@(node _ false ccs) => | some (node _ false ccs) =>
cs := cs.erase Name.cmp n cs := cs.erase Name.cmp n
node hn h.isFile (cs.insert Name.cmp n $ node n true ccs) node hn h.isFile (cs.insert Name.cmp n $ node n true ccs)
else else

View File

@ -4,25 +4,97 @@ import Cli
open DocGen4 Lean Cli open DocGen4 Lean Cli
def runDocGenCmd (p : Parsed) : IO UInt32 := do def findLeanInk? (p : Parsed) : IO (Option System.FilePath) := do
let modules : List String := p.variableArgsAs! String |>.toList
let res ← lakeSetup modules
match res with
| Except.ok (ws, leanHash) =>
IO.println s!"Loading modules from: {←searchPathRef.get}"
let doc ← load $ modules.map Name.mkSimple
IO.println "Outputting HTML"
match p.flag? "ink" with match p.flag? "ink" with
| some ink => | some ink =>
let inkPath := System.FilePath.mk ink.value let inkPath := System.FilePath.mk ink.value
if ←inkPath.pathExists then if ←inkPath.pathExists then
htmlOutput doc ws leanHash inkPath pure $ some inkPath
else else
throw $ IO.userError "Invalid path to LeanInk binary provided" throw $ IO.userError "Invalid path to LeanInk binary provided"
| none => htmlOutput doc ws leanHash none | none => pure none
def getTopLevelModules (p : Parsed) : IO (List String) := do
let topLevelModules := p.variableArgsAs! String |>.toList
if topLevelModules.length == 0 then
throw $ IO.userError "No topLevelModules provided."
pure topLevelModules
def runInitCmd (p : Parsed) : IO UInt32 := do
let topLevelModules ← getTopLevelModules p
let res ← lakeSetup topLevelModules
match res with
| Except.ok _ =>
let modules := topLevelModules.map Name.mkSimple
let hierarchy ← loadInit modules
let baseConfig := getSimpleBaseContext hierarchy
htmlOutputSetup baseConfig
pure 0 pure 0
| Except.error rc => pure rc | Except.error rc => pure rc
def runSingleCmd (p : Parsed) : IO UInt32 := do
let topLevelModules ← getTopLevelModules p
let relevantModules := [p.positionalArg! "module" |>.as! String]
let res ← lakeSetup (relevantModules ++ topLevelModules)
match res with
| Except.ok ws =>
let relevantModules := relevantModules.map Name.mkSimple
let topLevelModules := topLevelModules.map Name.mkSimple
let (doc, hierarchy) ← load (.loadAllLimitAnalysis topLevelModules relevantModules)
IO.println "Outputting HTML"
let baseConfig := getSimpleBaseContext hierarchy
htmlOutputResults baseConfig doc ws (←findLeanInk? p)
pure 0
| Except.error rc => pure rc
def runFinalizeCmd (p : Parsed) : IO UInt32 := do
let topLevelModules ← getTopLevelModules p
let res ← lakeSetup topLevelModules
match res with
| Except.ok _ =>
let modules := topLevelModules.map Name.mkSimple
let hierarchy ← loadInit modules
let baseConfig := getSimpleBaseContext hierarchy
htmlOutputFinalize baseConfig
pure 0
| Except.error rc => pure rc
pure 0
def runDocGenCmd (p : Parsed) : IO UInt32 := do
let modules : List String := p.variableArgsAs! String |>.toList
if modules.length == 0 then
throw $ IO.userError "No modules provided."
let res ← lakeSetup modules
match res with
| Except.ok ws =>
IO.println s!"Loading modules from: {←searchPathRef.get}"
let modules := modules.map Name.mkSimple
let (doc, hierarchy) ← load (.loadAll modules)
IO.println "Outputting HTML"
htmlOutput doc hierarchy ws (←findLeanInk? p)
pure 0
| Except.error rc => pure rc
def singleCmd := `[Cli|
single VIA runSingleCmd;
"Only generate the documentation for the module it was given, might contain broken links unless all documentation is generated."
FLAGS:
ink : String; "Path to a LeanInk binary to use for rendering the Lean sources."
ARGS:
module : String; "The module to generate the HTML for. Does not have to be part of topLevelModules."
...topLevelModules : String; "The top level modules this documentation will be for."
]
def finalizeCmd := `[Cli|
finalize VIA runFinalizeCmd;
"Finalize the documentation that has been generated by single."
ARGS:
...topLevelModule : String; "The top level modules this documentation will be for."
]
def docGenCmd : Cmd := `[Cli| def docGenCmd : Cmd := `[Cli|
"doc-gen4" VIA runDocGenCmd; ["0.0.1"] "doc-gen4" VIA runDocGenCmd; ["0.0.1"]
"A documentation generator for Lean 4." "A documentation generator for Lean 4."
@ -31,7 +103,11 @@ def docGenCmd : Cmd := `[Cli|
ink : String; "Path to a LeanInk binary to use for rendering the Lean sources." ink : String; "Path to a LeanInk binary to use for rendering the Lean sources."
ARGS: ARGS:
...modules : String; "The modules to generate the HTML for" ...modules : String; "The modules to generate the HTML for."
SUBCOMMANDS:
singleCmd;
finalizeCmd
] ]
def main (args : List String) : IO UInt32 := def main (args : List String) : IO UInt32 :=

View File

@ -18,3 +18,32 @@ You could e.g. host the files locally with the built-in Python webserver:
```sh ```sh
$ cd build/doc && python -m http.server $ cd build/doc && python -m http.server
``` ```
### Multi stage
You can also use `doc-gen4` in multiple separate stages to generate the whole documentation.
For example `mathlib4` consists out of 4 modules, the 3 Lean compiler ones and itself:
- `Init`
- `Std`
- `Lean`
- `Mathlib`
The first build stage is to run doc-gen for all modules separately:
1. `doc-gen4 single Init Mathlib`
2. `doc-gen4 single Std Mathlib`
3. `doc-gen4 single Lean Mathlib`
4. `doc-gen4 single Mathlib Mathlib`
We have to pass the `Mathlib` top level module on each invocation here so
it can generate the navbar on the left hand side properly, it will only
generate documentation for its first argument module.
Furthermore one can use the `--ink` flag here to also generate LeanInk
documentation in addition.
The second and last stage is the finalize one which zips up some
information relevant for the search:
```sh
$ doc-gen4 finalize Mathlib
```
Now `build/doc` should contain the same files with the same context as if one had run
```
$ doc-gen4 Mathlib
```

View File

@ -6,6 +6,17 @@
const CACHE_DB_NAME = "declaration-data"; const CACHE_DB_NAME = "declaration-data";
const CACHE_DB_VERSION = 1; const CACHE_DB_VERSION = 1;
const CACHE_DB_KEY = "DECLARATIONS_KEY";
async function fetchModuleData(module) {
const moduleDataUrl = new URL(
`${SITE_ROOT}declaration-data-${module}.bmp`,
window.location
);
const moduleData = await fetch(moduleDataUrl);
const moduleDataJson = await moduleData.json();
return moduleDataJson;
}
/** /**
* The DeclarationDataCenter is used for declaration searching. * The DeclarationDataCenter is used for declaration searching.
@ -41,42 +52,39 @@ export class DeclarationDataCenter {
*/ */
static async init() { static async init() {
if (!DeclarationDataCenter.singleton) { if (!DeclarationDataCenter.singleton) {
const timestampUrl = new URL( const dataListUrl = new URL(
`${SITE_ROOT}declaration-data.timestamp`,
window.location
);
const dataUrl = new URL(
`${SITE_ROOT}declaration-data.bmp`, `${SITE_ROOT}declaration-data.bmp`,
window.location window.location
); );
const timestampRes = await fetch(timestampUrl);
const timestamp = await timestampRes.text();
// try to use cache first // try to use cache first
const data = await fetchCachedDeclarationData(timestamp).catch(_e => null); const data = await fetchCachedDeclarationData().catch(_e => null);
if (data) { if (data) {
// if data is defined, use the cached one. // if data is defined, use the cached one.
DeclarationDataCenter.singleton = new DeclarationDataCenter(data); DeclarationDataCenter.singleton = new DeclarationDataCenter(data);
} else { } else {
// undefined. then fetch the data from the server. // undefined. then fetch the data from the server.
const dataRes = await fetch(dataUrl); const dataListRes = await fetch(dataListUrl);
const dataJson = await dataRes.json(); const dataListJson = await dataListRes.json();
// TODO: this is probably kind of inefficient
const dataJsonUnflattened = await Promise.all(dataListJson.map(fetchModuleData));
const dataJson = dataJsonUnflattened.flat();
// the data is a map of name (original case) to declaration data. // the data is a map of name (original case) to declaration data.
const data = new Map( const data = new Map(
dataJson.map(({ name, doc, link, docLink, sourceLink }) => [ dataJson.map(({ name, doc, docLink, sourceLink }) => [
name, name,
{ {
name, name,
lowerName: name.toLowerCase(), lowerName: name.toLowerCase(),
lowerDoc: doc.toLowerCase(), lowerDoc: doc.toLowerCase(),
link,
docLink, docLink,
sourceLink, sourceLink,
}, },
]) ])
); );
await cacheDeclarationData(timestamp, data); await cacheDeclarationData(data);
DeclarationDataCenter.singleton = new DeclarationDataCenter(data); DeclarationDataCenter.singleton = new DeclarationDataCenter(data);
} }
} }
@ -137,7 +145,6 @@ function getMatches(declarations, pattern, maxResults = 30) {
name, name,
lowerName, lowerName,
lowerDoc, lowerDoc,
link,
docLink, docLink,
sourceLink, sourceLink,
} of declarations.values()) { } of declarations.values()) {
@ -156,7 +163,6 @@ function getMatches(declarations, pattern, maxResults = 30) {
err, err,
lowerName, lowerName,
lowerDoc, lowerDoc,
link,
docLink, docLink,
sourceLink, sourceLink,
}); });
@ -195,17 +201,16 @@ async function getDeclarationDatabase() {
/** /**
* Store data in indexedDB object store. * Store data in indexedDB object store.
* @param {string} timestamp
* @param {Map<string, any>} data * @param {Map<string, any>} data
*/ */
async function cacheDeclarationData(timestamp, data) { async function cacheDeclarationData(data) {
let db = await getDeclarationDatabase(); let db = await getDeclarationDatabase();
let store = db let store = db
.transaction("declaration", "readwrite") .transaction("declaration", "readwrite")
.objectStore("declaration"); .objectStore("declaration");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let clearRequest = store.clear(); let clearRequest = store.clear();
let addRequest = store.add(data, timestamp); let addRequest = store.add(data, CACHE_DB_KEY);
addRequest.onsuccess = function (event) { addRequest.onsuccess = function (event) {
resolve(); resolve();
@ -221,16 +226,15 @@ async function cacheDeclarationData(timestamp, data) {
/** /**
* Retrieve data from indexedDB database. * Retrieve data from indexedDB database.
* @param {string} timestamp
* @returns {Promise<Map<string, any>|undefined>} * @returns {Promise<Map<string, any>|undefined>}
*/ */
async function fetchCachedDeclarationData(timestamp) { async function fetchCachedDeclarationData() {
let db = await getDeclarationDatabase(); let db = await getDeclarationDatabase();
let store = db let store = db
.transaction("declaration", "readonly") .transaction("declaration", "readonly")
.objectStore("declaration"); .objectStore("declaration");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let transactionRequest = store.get(timestamp); let transactionRequest = store.get(CACHE_DB_KEY);
transactionRequest.onsuccess = function (event) { transactionRequest.onsuccess = function (event) {
resolve(event.result); resolve(event.result);
}; };