nitdoc: generate pages with bootstrap
authorAlexandre Terrasa <alexandre@moz-code.org>
Fri, 6 Jun 2014 07:24:25 +0000 (03:24 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Fri, 6 Jun 2014 18:43:42 +0000 (14:43 -0400)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

src/doc/doc_pages.nit [new file with mode: 0644]

diff --git a/src/doc/doc_pages.nit b/src/doc/doc_pages.nit
new file mode 100644 (file)
index 0000000..e41cae2
--- /dev/null
@@ -0,0 +1,1143 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Nitdoc page generation
+module doc_pages
+
+import doc_model
+
+# The NitdocContext contains all the knowledge used for doc generation
+class NitdocContext
+       private var opt_dir = new OptionString("output directory", "-d", "--dir")
+       private var opt_source = new OptionString("link for source (%f for filename, %l for first line, %L for last line)", "--source")
+       private var opt_sharedir = new OptionString("directory containing nitdoc assets", "--sharedir")
+       private var opt_shareurl = new OptionString("use shareurl instead of copy shared files", "--shareurl")
+       private var opt_nodot = new OptionBool("do not generate graphes with graphviz", "--no-dot")
+       private var opt_private = new OptionBool("also generate private API", "--private")
+
+       private var opt_custom_title = new OptionString("custom title for homepage", "--custom-title")
+       private var opt_custom_menu = new OptionString("custom items added in top menu (each item must be enclosed in 'li' tags)", "--custom-menu-items")
+       private var opt_custom_intro = new OptionString("custom intro text for homepage", "--custom-overview-text")
+       private var opt_custom_footer = new OptionString("custom footer text", "--custom-footer-text")
+
+       private var opt_github_upstream = new OptionString("Git branch where edited commits will be pulled into (ex: user:repo:branch)", "--github-upstream")
+       private var opt_github_base_sha1 = new OptionString("Git sha1 of base commit used to create pull request", "--github-base-sha1")
+       private var opt_github_gitdir = new OptionString("Git working directory used to resolve path name (ex: /home/me/myproject/)", "--github-gitdir")
+
+       private var opt_piwik_tracker = new OptionString("Piwik tracker URL (ex: nitlanguage.org/piwik/)", "--piwik-tracker")
+       private var opt_piwik_site_id = new OptionString("Piwik site ID", "--piwik-site-id")
+
+       private var toolcontext = new ToolContext
+       private var mbuilder: ModelBuilder
+       private var mainmodule: MModule
+       private var output_dir: String
+       private var min_visibility: MVisibility
+
+       init do
+               var opts = toolcontext.option_context
+               opts.add_option(opt_dir, opt_source, opt_sharedir, opt_shareurl, opt_nodot, opt_private)
+               opts.add_option(opt_custom_title, opt_custom_footer, opt_custom_intro, opt_custom_menu)
+               opts.add_option(opt_github_upstream, opt_github_base_sha1, opt_github_gitdir)
+               opts.add_option(opt_piwik_tracker, opt_piwik_site_id)
+
+               var tpl = new Template
+               tpl.add "Usage: nitdoc [OPTION]... <file.nit>...\n"
+               tpl.add "Generates HTML pages of API documentation from Nit source files."
+               toolcontext.tooldescription = tpl.write_to_string
+               toolcontext.process_options(args)
+
+               self.process_options
+               self.parse(toolcontext.option_context.rest)
+       end
+
+       private fun process_options do
+               if opt_private.value then
+                       min_visibility = none_visibility
+               else
+                       min_visibility = protected_visibility
+               end
+               var gh_upstream = opt_github_upstream.value
+               var gh_base_sha = opt_github_base_sha1.value
+               var gh_gitdir = opt_github_gitdir.value
+               if not gh_upstream == null or not gh_base_sha == null or not gh_gitdir == null then
+                       if gh_upstream == null or gh_base_sha == null or gh_gitdir == null then
+                               print "Error: Options {opt_github_upstream.names.first}, {opt_github_base_sha1.names.first} and {opt_github_gitdir.names.first} are required to enable the GitHub plugin"
+                               abort
+                       end
+               end
+       end
+
+       private fun parse(arguments: Array[String]) do
+               var model = new Model
+               mbuilder = new ModelBuilder(model, toolcontext)
+               var mmodules = mbuilder.parse(arguments)
+               if mmodules.is_empty then return
+               mbuilder.run_phases
+               if mmodules.length == 1 then
+                       mainmodule = mmodules.first
+               else
+                       mainmodule = new MModule(model, null, "<main>", new Location(null, 0, 0, 0, 0))
+                       mainmodule.is_fictive = true
+                       mainmodule.set_imported_mmodules(mmodules)
+               end
+       end
+
+       fun generate_nitdoc do
+               init_output_dir
+               overview
+               search
+               modules
+               classes
+               quicksearch_list
+       end
+
+       private fun init_output_dir do
+               # location output dir
+               var output_dir = opt_dir.value
+               if output_dir == null then
+                       output_dir = "doc"
+               end
+               self.output_dir = output_dir
+               # create destination dir if it's necessary
+               if not output_dir.file_exists then output_dir.mkdir
+               # locate share dir
+               var sharedir = opt_sharedir.value
+               if sharedir == null then
+                       var dir = toolcontext.nit_dir
+                       if dir == null then
+                               print "Error: Cannot locate nitdoc share files. Uses --sharedir or envvar NIT_DIR"
+                               abort
+                       end
+                       sharedir = "{dir}/share/nitdoc"
+                       if not sharedir.file_exists then
+                               print "Error: Cannot locate nitdoc share files. Uses --sharedir or envvar NIT_DIR"
+                               abort
+                       end
+               end
+               # copy shared files
+               if opt_shareurl.value == null then
+                       sys.system("cp -r {sharedir.to_s}/* {output_dir.to_s}/")
+               else
+                       sys.system("cp -r {sharedir.to_s}/resources/ {output_dir.to_s}/resources/")
+               end
+
+       end
+
+       private fun overview do
+               var overviewpage = new NitdocOverview(self)
+               overviewpage.render.write_to_file("{output_dir.to_s}/index.html")
+       end
+
+       private fun search do
+               var searchpage = new NitdocSearch(self)
+               searchpage.render.write_to_file("{output_dir.to_s}/search.html")
+       end
+
+       private fun modules do
+               for mmodule in mbuilder.model.mmodules do
+                       if mmodule.name == "<main>" then continue
+                       var modulepage = new NitdocModule(mmodule, self)
+                       modulepage.render.write_to_file("{output_dir.to_s}/{mmodule.nitdoc_url}")
+               end
+       end
+
+       private fun classes do
+               for mclass in mbuilder.model.mclasses do
+                       var classpage = new NitdocClass(mclass, self)
+                       classpage.render.write_to_file("{output_dir.to_s}/{mclass.nitdoc_url}")
+               end
+       end
+
+       private fun quicksearch_list do
+               var quicksearch = new QuickSearch(self)
+               quicksearch.render.write_to_file("{output_dir.to_s}/quicksearch-list.js")
+       end
+end
+
+# Nitdoc QuickSearch list generator
+#
+# Create a JSON object containing links to:
+#  * modules
+#  * mclasses
+#  * mpropdefs
+# All entities are grouped by name to make the research easier.
+class QuickSearch
+
+       private var mmodules = new HashSet[MModule]
+       private var mclasses = new HashSet[MClass]
+       private var mpropdefs = new HashMap[String, Set[MPropDef]]
+
+       init(ctx: NitdocContext) do
+               for mmodule in ctx.mbuilder.model.mmodules do
+                       if mmodule.name == "<main>" then continue
+                       mmodules.add mmodule
+               end
+               for mclass in ctx.mbuilder.model.mclasses do
+                       if mclass.visibility < ctx.min_visibility then continue
+                       mclasses.add mclass
+               end
+               for mproperty in ctx.mbuilder.model.mproperties do
+                       if mproperty.visibility < ctx.min_visibility then continue
+                       if mproperty isa MAttribute then continue
+                       if not mpropdefs.has_key(mproperty.name) then
+                               mpropdefs[mproperty.name] = new HashSet[MPropDef]
+                       end
+                       mpropdefs[mproperty.name].add_all(mproperty.mpropdefs)
+               end
+       end
+
+       fun render: Template do
+               var tpl = new Template
+               tpl.add "var nitdocQuickSearchRawList=\{ "
+               for mmodule in mmodules do
+                       tpl.add "\"{mmodule.name}\":["
+                       tpl.add "\{txt:\"{mmodule.full_name}\",url:\"{mmodule.nitdoc_url}\"\},"
+                       tpl.add "],"
+               end
+               for mclass in mclasses do
+                       var full_name = mclass.intro.mmodule.full_name
+                       tpl.add "\"{mclass.name}\":["
+                       tpl.add "\{txt:\"{full_name}\",url:\"{mclass.nitdoc_url}\"\},"
+                       tpl.add "],"
+               end
+               for mproperty, mprops in mpropdefs do
+                       tpl.add "\"{mproperty}\":["
+                       for mpropdef in mprops do
+                               var full_name = mpropdef.mclassdef.mclass.full_name
+                               tpl.add "\{txt:\"{full_name}\",url:\"{mpropdef.nitdoc_url}\"\},"
+                       end
+                       tpl.add "],"
+               end
+               tpl.add " \};"
+               return tpl
+       end
+end
+
+# Nitdoc base page
+# Define page structure and properties
+abstract class NitdocPage
+
+       private var ctx: NitdocContext
+       private var model: Model
+
+       init(ctx: NitdocContext) do
+               self.ctx = ctx
+               self.model = ctx.mbuilder.model
+       end
+
+       # Render the page as a html template
+       fun render: Template do
+               var shareurl = "."
+               if ctx.opt_shareurl.value != null then
+                       shareurl = ctx.opt_shareurl.value.as(not null)
+               end
+
+               # build page
+               var tpl = tpl_page
+               tpl.title = tpl_title
+               tpl.shareurl = shareurl
+               tpl.topmenu = tpl_topmenu
+               tpl_content
+               tpl.footer = ctx.opt_custom_footer.value
+               tpl.body_attrs.add(new TagAttribute("data-bootstrap-share", shareurl))
+               tpl.sidebar = tpl_sidebar
+
+               # piwik tracking
+               var tracker_url = ctx.opt_piwik_tracker.value
+               var site_id = ctx.opt_piwik_site_id.value
+               if tracker_url != null and site_id != null then
+                       tpl.scripts.add new TplPiwikScript(tracker_url, site_id)
+               end
+               return tpl
+       end
+
+       # Build page template
+       fun tpl_page: TplPage is abstract
+
+       # Build page sidebar if any
+       fun tpl_sidebar: nullable TplSidebar do return null
+
+       # Build page title string
+       fun tpl_title: String do
+               if ctx.opt_custom_title.value != null then
+                       return ctx.opt_custom_title.value.to_s
+               end
+               return "Nitdoc"
+       end
+
+       # Build top menu template
+       fun tpl_topmenu: TplTopMenu do
+               var topmenu = new TplTopMenu
+               var custom_elt = ctx.opt_custom_menu.value
+               if custom_elt != null then topmenu.add_raw(custom_elt)
+               return topmenu
+       end
+
+       # Build page content template
+       fun tpl_content is abstract
+
+       # Clickable graphviz image using dot format
+       # return null if no graph for this page
+       fun tpl_graph(dot: FlatBuffer, name: String, title: String): nullable TplArticle do
+               if ctx.opt_nodot.value then return null
+               var output_dir = ctx.output_dir
+               var file = new OFStream.open("{output_dir}/{name}.dot")
+               file.write(dot)
+               file.close
+               sys.system("\{ test -f {output_dir}/{name}.png && test -f {output_dir}/{name}.s.dot && diff {output_dir}/{name}.dot {output_dir}/{name}.s.dot >/dev/null 2>&1 ; \} || \{ cp {output_dir}/{name}.dot {output_dir}/{name}.s.dot && dot -Tpng -o{output_dir}/{name}.png -Tcmapx -o{output_dir}/{name}.map {output_dir}/{name}.s.dot ; \}")
+               var fmap = new IFStream.open("{output_dir}/{name}.map")
+               var map = fmap.read_all
+               fmap.close
+
+               var article = new TplArticle.with_title("graph", title)
+               var content = new Template
+               content.add "<img src='{name}.png' usemap='#{name}' style='margin:auto' alt='{title}'/>"
+               content.add map
+               article.content = content
+               return article
+       end
+
+       # A (source) link template for a given location
+       fun tpl_showsource(location: nullable Location): nullable String
+       do
+               if location == null then return null
+               var source = ctx.opt_source.value
+               if source == null then return "({location.file.filename.simplify_path})"
+               # THIS IS JUST UGLY ! (but there is no replace yet)
+               var x = source.split_with("%f")
+               source = x.join(location.file.filename.simplify_path)
+               x = source.split_with("%l")
+               source = x.join(location.line_start.to_s)
+               x = source.split_with("%L")
+               source = x.join(location.line_end.to_s)
+               source = source.simplify_path
+               return " (<a target='_blank' title='Show source' href=\"{source.to_s}\">source</a>)"
+       end
+
+       # MClassDef description template
+       fun tpl_mclass_article(mclass: MClass, mclassdefs: Array[MClassDef]): TplArticle do
+               var article = new TplArticle(mclass.nitdoc_anchor)
+               var title = new Template
+               var icon = new TplIcon.with_icon("tag")
+               icon.css_classes.add_all(mclass.intro.tpl_css_classes)
+               title.add icon
+               title.add mclass.tpl_link
+               title.add mclass.intro.tpl_signature
+               article.title = title
+               article.title_classes.add "signature"
+               article.subtitle = mclass.tpl_declaration
+               article.summary_title = "{mclass.nitdoc_name}{mclass.tpl_signature.write_to_string}"
+               #article.subtitle = new Template
+               #article.subtitle.add mprop.intro.tpl_modifiers
+               #article.subtitle.add mprop.intro.tpl_namespace
+               var content = new Template
+
+               if not mclassdefs.has(mclass.intro) then
+                       # add intro synopsys
+                       var intro = mclass.intro
+                       var location = intro.location
+                       var sourcelink = tpl_showsource(location)
+                       var intro_def = intro.tpl_definition
+                       intro_def.location = sourcelink
+                       content.add intro_def
+               end
+               ctx.mainmodule.linearize_mclassdefs(mclassdefs)
+               for mclassdef in mclassdefs do
+                       # add mclassdef full description
+                       var location = mclassdef.location
+                       var sourcelink = tpl_showsource(location)
+                       var prop_def = mclassdef.tpl_definition.as(TplClassDefinition)
+                       prop_def.location = sourcelink
+                       for mpropdef in mclassdef.mpropdefs do
+                               var intro = mpropdef.mproperty.intro
+                               if mpropdef isa MAttributeDef then continue
+                               if mpropdef.mproperty.visibility < ctx.min_visibility then continue
+
+                               var lnk = new Template
+                               lnk.add new TplLabel.with_classes(mpropdef.tpl_css_classes.to_a)
+                               lnk.add mpropdef.tpl_link
+                               if intro.mdoc != null then
+                                       lnk.add ": "
+                                       lnk.add intro.mdoc.short_comment
+                               end
+                               if mpropdef.is_intro then
+                                       prop_def.intros.add new TplListItem.with_content(lnk)
+                               else
+                                       prop_def.redefs.add new TplListItem.with_content(lnk)
+                               end
+                       end
+                       content.add prop_def
+               end
+               article.content = content
+               return article
+       end
+
+       # MProp description template
+       fun tpl_mprop_article(mprop: MProperty, mpropdefs: Array[MPropDef]): TplArticle do
+               var article = new TplArticle(mprop.intro.nitdoc_anchor)
+               var icon = new TplIcon.with_icon("tag")
+               icon.css_classes.add_all(mprop.intro.tpl_css_classes)
+               var title = new Template
+               title.add icon
+               title.add mprop.nitdoc_name
+               title.add mprop.intro.tpl_signature
+               article.title = title
+               article.title_classes.add "signature"
+               article.subtitle = mprop.tpl_declaration
+               article.summary_title = mprop.nitdoc_name
+               #article.subtitle = new Template
+               #article.subtitle.add mprop.intro.tpl_modifiers
+               #article.subtitle.add mprop.intro.tpl_namespace
+               var content = new Template
+
+               if not mpropdefs.has(mprop.intro) then
+                       # add intro synopsys
+                       var intro = mprop.intro
+                       var location = intro.location
+                       var sourcelink = tpl_showsource(location)
+                       var intro_def = intro.tpl_definition
+                       intro_def.location = sourcelink
+                       content.add intro_def
+               end
+
+               ctx.mainmodule.linearize_mpropdefs(mpropdefs)
+               for mpropdef in mpropdefs do
+                       # add mpropdef description
+                       var location = mpropdef.location
+                       var sourcelink = tpl_showsource(location)
+                       var prop_def = mpropdef.tpl_definition
+                       prop_def.location = sourcelink
+                       content.add prop_def
+               end
+               article.content = content
+               return article
+       end
+end
+
+# The overview page
+# Display a list of modules contained in program
+class NitdocOverview
+       super NitdocPage
+
+       init(ctx: NitdocContext) do super(ctx)
+
+       private var page = new TplPage
+       redef fun tpl_page do return page
+
+       private var sidebar = new TplSidebar
+       redef fun tpl_sidebar do return sidebar
+
+       redef fun tpl_title do
+               if ctx.opt_custom_title.value != null then
+                       return ctx.opt_custom_title.value.to_s
+               else
+                       return "Overview"
+               end
+       end
+
+       redef fun tpl_topmenu do
+               var topmenu = super
+               topmenu.add_link("#", "Overview", true)
+               topmenu.add_link("search.html", "Index", false)
+               return topmenu
+       end
+
+       # intro text
+       private fun tpl_intro: TplSection do
+               var section = new TplSection.with_title("overview", tpl_title)
+               var article = new TplArticle("intro")
+               if ctx.opt_custom_intro.value != null then
+                       article.content = ctx.opt_custom_intro.value.to_s
+               end
+               section.add_child article
+               return section
+       end
+
+       # projects list
+       private fun tpl_projects(section: TplSection) do
+               # Projects list
+               var ssection = new TplSection.with_title("projects", "Projects")
+               for mproject in model.mprojects do
+                       ssection.add_child mproject.tpl_article
+               end
+               section.add_child ssection
+       end
+
+       redef fun tpl_content do
+               var top = tpl_intro
+               tpl_projects(top)
+               tpl_page.add_section top
+       end
+end
+
+# The search page
+# Display a list of modules, classes and properties
+class NitdocSearch
+       super NitdocPage
+
+       init(ctx: NitdocContext) do super(ctx)
+
+       private var page = new TplPage
+       redef fun tpl_page do return page
+
+       redef fun tpl_title do return "Index"
+
+       redef fun tpl_topmenu do
+               var topmenu = super
+               topmenu.add_link("index.html", "Overview", false)
+               topmenu.add_link("#", "Index", true)
+               return topmenu
+       end
+
+       redef fun tpl_content do
+               var tpl = new TplSearchPage("search_all")
+               var section = new TplSection("search")
+               # title
+               tpl.title = "Index"
+               # modules list
+               for mmodule in modules_list do
+                       tpl.modules.add mmodule.tpl_link
+               end
+               # classes list
+               for mclass in classes_list do
+                       tpl.classes.add mclass.tpl_link
+               end
+               # properties list
+               for mproperty in mprops_list do
+                       var m = new Template
+                       m.add mproperty.intro.tpl_link
+                       m.add " ("
+                       m.add mproperty.intro.mclassdef.mclass.tpl_link
+                       m.add ")"
+                       tpl.props.add m
+               end
+               section.add_child tpl
+               tpl_page.add_section section
+       end
+
+       # Extract mmodule list to display (sorted by name)
+       private fun modules_list: Array[MModule] do
+               var sorted = new Array[MModule]
+               for mmodule in ctx.mbuilder.model.mmodule_importation_hierarchy do
+                       if mmodule.name == "<main>" then continue
+                       sorted.add mmodule
+               end
+               var sorter = new MModuleNameSorter
+               sorter.sort(sorted)
+               return sorted
+       end
+
+       # Extract mclass list to display (sorted by name)
+       private fun classes_list: Array[MClass] do
+               var sorted = new Array[MClass]
+               for mclass in ctx.mbuilder.model.mclasses do
+                       if mclass.visibility < ctx.min_visibility then continue
+                       sorted.add mclass
+               end
+               var sorter = new MClassNameSorter
+               sorter.sort(sorted)
+               return sorted
+       end
+
+       # Extract mproperty list to display (sorted by name)
+       private fun mprops_list: Array[MProperty] do
+               var sorted = new Array[MProperty]
+               for mproperty in ctx.mbuilder.model.mproperties do
+                       if mproperty.visibility < ctx.min_visibility then continue
+                       if mproperty isa MAttribute then continue
+                       sorted.add mproperty
+               end
+               var sorter = new MPropertyNameSorter
+               sorter.sort(sorted)
+               return sorted
+       end
+end
+
+# A module page
+# Display the list of introduced and redefined classes in module
+class NitdocModule
+       super NitdocPage
+
+       private var mmodule: MModule
+
+       init(mmodule: MModule, ctx: NitdocContext) do
+               self.mmodule = mmodule
+               super(ctx)
+       end
+
+       private var page = new TplPage
+       redef fun tpl_page do return page
+
+       private var sidebar = new TplSidebar
+       redef fun tpl_sidebar do return sidebar
+
+       redef fun tpl_title do return "{mmodule.nitdoc_name}"
+
+       redef fun tpl_topmenu do
+               var topmenu = super
+               topmenu.add_link("index.html", "Overview", false)
+               topmenu.add_link("#", "{mmodule.nitdoc_name}", true)
+               topmenu.add_link("search.html", "Index", false)
+               return topmenu
+       end
+
+       # intro text
+       private fun tpl_intro: TplSection do
+               var section = new TplSection.with_title(mmodule.nitdoc_anchor, tpl_title)
+               section.subtitle = mmodule.tpl_declaration
+
+               var article = new TplArticle("intro")
+               var def = mmodule.tpl_definition
+               var location = mmodule.location
+               def.location = tpl_showsource(location)
+               article.content = def
+               section.add_child article
+               return section
+       end
+
+       # inheritance section
+       private fun tpl_inheritance(parent: TplSection) do
+               # Extract relevent modules
+               var nested = mmodule.in_nesting.direct_greaters.to_a
+               var imports = mmodule.in_importation.greaters
+               if imports.length > 10 then imports = mmodule.in_importation.direct_greaters
+               var clients = mmodule.in_importation.smallers
+               if clients.length > 10 then clients = mmodule.in_importation.direct_smallers
+
+               # Display lists
+               var sorter = new MModuleNameSorter
+               var section = new TplSection.with_title("inheritance", "Inheritance")
+
+               # Graph
+               var mmodules = new HashSet[MModule]
+               mmodules.add_all nested
+               mmodules.add_all imports
+               if clients.length < 10 then mmodules.add_all clients
+               mmodules.add mmodule
+               var graph = tpl_dot(mmodules)
+               if graph != null then section.add_child graph
+
+               # nested modules
+               if not nested.is_empty then
+                       var lst = nested.to_a
+                       sorter.sort lst
+                       section.add_child tpl_list("nesting", "Nested modules", lst)
+               end
+
+               # Imports
+               var lst = new Array[MModule]
+               for dep in imports do
+                       if dep.is_fictive then continue
+                       if dep == mmodule then continue
+                       lst.add(dep)
+               end
+               if not lst.is_empty then
+                       sorter.sort lst
+                       section.add_child tpl_list("imports", "Imports", lst)
+               end
+
+               # Clients
+               lst = new Array[MModule]
+               for dep in clients do
+                       if dep.is_fictive then continue
+                       if dep == mmodule then continue
+                       lst.add(dep)
+               end
+               if not lst.is_empty then
+                       sorter.sort lst
+                       section.add_child tpl_list("clients", "Clients", lst)
+               end
+
+               parent.add_child section
+       end
+
+       private fun tpl_list(id: String, title: String, mmodules: Array[MModule]): TplArticle do
+               var article = new TplArticle.with_title(id, title)
+               var list = new TplList.with_classes(["list-unstyled", "list-definition"])
+               for mmodule in mmodules do list.elts.add mmodule.tpl_list_item
+               article.content = list
+               return article
+       end
+
+       private fun tpl_mclasses(parent: TplSection) do
+               var sorter = new MClassNameSorter
+               var mclassdefs = new HashSet[MClassDef]
+               mclassdefs.add_all mmodule.in_nesting_intro_mclassdefs(ctx.min_visibility)
+               mclassdefs.add_all mmodule.in_nesting_redef_mclassdefs(ctx.min_visibility)
+               var mclasses2mdefs = sort_by_mclass(mclassdefs)
+               var sorted_mclasses = mclasses2mdefs.keys.to_a
+               sorter.sort sorted_mclasses
+
+               # intros
+               var section = new TplSection.with_title("intros", "Introductions")
+               var intros = mmodule.in_nesting_intro_mclasses(ctx.min_visibility)
+               var sorted_intros = intros.to_a
+               sorter.sort(sorted_intros)
+               for mclass in sorted_intros do
+                       if not mclasses2mdefs.has_key(mclass) then continue
+                       section.add_child tpl_mclass_article(mclass, mclasses2mdefs[mclass].to_a)
+               end
+               parent.add_child section
+
+               # redefs
+               section = new TplSection.with_title("redefs", "Refinements")
+               var redefs = mmodule.in_nesting_redef_mclasses(ctx.min_visibility).to_a
+               sorter.sort(redefs)
+               for mclass in redefs do
+                       if intros.has(mclass) then continue
+                       if not mclasses2mdefs.has_key(mclass) then continue
+                       section.add_child tpl_mclass_article(mclass, mclasses2mdefs[mclass].to_a)
+               end
+               parent.add_child section
+       end
+
+       redef fun tpl_content do
+               var top = tpl_intro
+               tpl_inheritance(top)
+               tpl_mclasses(top)
+               tpl_page.add_section top
+       end
+
+       # Genrate dot hierarchy for class inheritance
+       fun tpl_dot(mmodules: Collection[MModule]): nullable TplArticle do
+               var poset = new POSet[MModule]
+               for mmodule in mmodules do
+                       if mmodule.is_fictive then continue
+                       poset.add_node mmodule
+                       for omodule in mmodules do
+                               if mmodule.is_fictive then continue
+                               poset.add_node mmodule
+                               if mmodule.in_importation < omodule then
+                                       poset.add_edge(mmodule, omodule)
+                               end
+                       end
+               end
+               # build graph
+               var op = new FlatBuffer
+               var name = "dep_{mmodule.name}"
+               op.append("digraph {name} \{ rankdir=BT; node[shape=none,margin=0,width=0,height=0,fontsize=10]; edge[dir=none,color=gray]; ranksep=0.2; nodesep=0.1;\n")
+               for mmodule in poset do
+                       if mmodule == self.mmodule then
+                               op.append("\"{mmodule.name}\"[shape=box,margin=0.03];\n")
+                       else
+                               op.append("\"{mmodule.name}\"[URL=\"{mmodule.nitdoc_url}\"];\n")
+                       end
+                       for omodule in poset[mmodule].direct_greaters do
+                               op.append("\"{mmodule.name}\"->\"{omodule.name}\";\n")
+                       end
+               end
+               op.append("\}\n")
+               return tpl_graph(op, name, "Dependency graph")
+       end
+
+       private fun sort_by_mclass(mclassdefs: Collection[MClassDef]): Map[MClass, Set[MClassDef]] do
+               var map = new HashMap[MClass, Set[MClassDef]]
+               for mclassdef in mclassdefs do
+                       var mclass = mclassdef.mclass
+                       if not map.has_key(mclass) then map[mclass] = new HashSet[MClassDef]
+                       map[mclass].add mclassdef
+               end
+               return map
+       end
+end
+
+# A class page
+# Display a list properties defined or redefined for this class
+class NitdocClass
+       super NitdocPage
+
+       private var mclass: MClass
+       private var mprops2mdefs: Map[MProperty, Set[MPropDef]]
+
+       init(mclass: MClass, ctx: NitdocContext) do
+               self.mclass = mclass
+               super(ctx)
+               var mpropdefs = new HashSet[MPropDef]
+               mpropdefs.add_all mclass.intro_mpropdefs(ctx.min_visibility)
+               mpropdefs.add_all mclass.redef_mpropdefs(ctx.min_visibility)
+               mprops2mdefs = sort_by_mproperty(mpropdefs)
+       end
+
+       private var page = new TplPage
+       redef fun tpl_page do return page
+
+       private var sidebar = new TplSidebar
+       redef fun tpl_sidebar do return sidebar
+
+       redef fun tpl_title do return "{mclass.nitdoc_name}{mclass.tpl_signature.write_to_string}"
+
+       redef fun tpl_topmenu do
+               var topmenu = super
+               var mmodule: MModule
+               if mclass.public_owner == null then
+                       mmodule = mclass.intro_mmodule
+               else
+                       mmodule = mclass.public_owner.as(not null)
+               end
+               topmenu.add_link("index.html", "Overview", false)
+               topmenu.add_link("{mmodule.nitdoc_url}", "{mmodule.nitdoc_name}", false)
+               topmenu.add_link("#", "{mclass.nitdoc_name}", true)
+               topmenu.add_link("search.html", "Index", false)
+               return topmenu
+       end
+
+       # Property list to display in sidebar
+       fun tpl_sidebar_properties do
+               var kind_map = sort_by_kind(mclass_inherited_mprops)
+               var summary = new TplList.with_classes(["list-unstyled"])
+
+               tpl_sidebar_list("Virtual types", kind_map["type"].to_a, summary)
+               tpl_sidebar_list("Constructors", kind_map["init"].to_a, summary)
+               tpl_sidebar_list("Methods", kind_map["fun"].to_a, summary)
+               tpl_sidebar.boxes.add new TplSideBox.with_content("All properties", summary)
+       end
+
+       var mprop_sorter = new MPropertyNameSorter
+
+       private fun tpl_sidebar_list(name: String, mprops: Array[MProperty], summary: TplList) do
+               if mprops.is_empty then return
+               mprop_sorter.sort(mprops)
+               var entry = new TplListItem.with_content(name)
+               var list = new TplList.with_classes(["list-unstyled", "list-labeled"])
+               for mprop in mprops do
+                       list.add_li tpl_sidebar_item(mprop)
+               end
+               entry.append list
+               summary.elts.add entry
+       end
+
+       private fun tpl_sidebar_item(mprop: MProperty): Template do
+               var classes = mprop.intro.tpl_css_classes.to_a
+               if not mprops2mdefs.has_key(mprop) then
+                       classes.add "inherit"
+                       var lnk = new Template
+                       lnk.add new TplLabel.with_classes(classes)
+                       lnk.add mprop.intro.tpl_link
+                       return lnk
+               end
+               var defs = mprops2mdefs[mprop]
+               if defs.has(mprop.intro) then
+                       classes.add "intro"
+               else
+                       classes.add "redef"
+               end
+               var lnk = new Template
+               lnk.add new TplLabel.with_classes(classes)
+               lnk.add mprop.intro.tpl_anchor
+               return lnk
+       end
+
+       private fun tpl_intro: TplSection do
+               var section = new TplSection.with_title(mclass.nitdoc_anchor, tpl_title)
+               section.subtitle = mclass.tpl_declaration
+               var article = new TplArticle("intro")
+               var intro = mclass.intro
+               var def = intro.tpl_definition
+               var location = intro.location
+               def.location = tpl_showsource(location)
+               article.content = def
+               section.add_child article
+               return section
+       end
+
+       private fun tpl_concerns(section: TplSection) do
+               var mmodules = collect_mmodules(mprops2mdefs.keys)
+               var owner_map = sort_by_public_owner(mmodules)
+               var owners = owner_map.keys.to_a
+
+               if not owners.is_empty then
+                       var article = new TplArticle.with_title("concerns", "Concerns")
+                       var module_sorter = new MModuleNameSorter
+                       module_sorter.sort owners
+                       var list = new TplList.with_classes(["list-unstyled", "list-definition"])
+                       for owner in owners do
+                               var li = new Template
+                               li.add owner.tpl_anchor
+                               if owner.mdoc != null then
+                                       li.add ": "
+                                       li.add owner.mdoc.short_comment
+                               end
+                               var smmodules = owner_map[owner].to_a
+                               #if not smmodules.length >= 1 then
+                                       var slist = new TplList.with_classes(["list-unstyled", "list-definition"])
+                                       module_sorter.sort smmodules
+                                       for mmodule in smmodules do
+                                               if mmodule == owner then continue
+                                               var sli = new Template
+                                               sli.add mmodule.tpl_anchor
+                                               if mmodule.mdoc != null then
+                                                       sli.add ": "
+                                                       sli.add mmodule.mdoc.short_comment
+                                               end
+                                               slist.add_li(sli)
+                                       end
+                                       li.add slist
+                                       list.add_li li
+                               #end
+                       end
+                       article.content = list
+                       section.add_child article
+               end
+       end
+
+       private fun tpl_inheritance(parent: TplSection) do
+               # parents
+               var hparents = new HashSet[MClass]
+               for c in mclass.in_hierarchy(ctx.mainmodule).direct_greaters do
+                       if c.visibility < ctx.min_visibility then continue
+                       hparents.add c
+               end
+
+               # ancestors
+               var hancestors = new HashSet[MClass]
+               for c in mclass.in_hierarchy(ctx.mainmodule).greaters do
+                       if c == mclass then continue
+                       if c.visibility < ctx.min_visibility then continue
+                       if hparents.has(c) then continue
+                       hancestors.add c
+               end
+
+               # children
+               var hchildren = new HashSet[MClass]
+               for c in mclass.in_hierarchy(ctx.mainmodule).direct_smallers do
+                       if c.visibility < ctx.min_visibility then continue
+                       hchildren.add c
+               end
+
+               # descendants
+               var hdescendants = new HashSet[MClass]
+               for c in mclass.in_hierarchy(ctx.mainmodule).smallers do
+                       if c == mclass then continue
+                       if c.visibility < ctx.min_visibility then continue
+                       if hchildren.has(c) then continue
+                       hdescendants.add c
+               end
+
+               # Display lists
+               var sorter = new MClassNameSorter
+               var section = new TplSection.with_title("inheritance", "Inheritance")
+
+               # Graph
+               var mclasses = new HashSet[MClass]
+               mclasses.add_all hancestors
+               mclasses.add_all hparents
+               if hchildren.length < 10 then mclasses.add_all hchildren
+               if hdescendants.length < 10 then mclasses.add_all hdescendants
+               mclasses.add mclass
+               var graph = tpl_dot(mclasses)
+               if graph != null then section.add_child graph
+
+               # parents
+               if not hparents.is_empty then
+                       var lst = hparents.to_a
+                       sorter.sort lst
+                       section.add_child tpl_list("parents", "Parents", lst)
+               end
+
+               # ancestors
+               if not hancestors.is_empty then
+                       var lst = hancestors.to_a
+                       sorter.sort lst
+                       section.add_child tpl_list("ancestors", "Ancestors", lst)
+               end
+
+               # children
+               if not hchildren.is_empty and hchildren.length < 15 then
+                       var lst = hchildren.to_a
+                       sorter.sort lst
+                       section.add_child tpl_list("children", "Children", lst)
+               end
+
+               # descendants
+               if not hdescendants.is_empty and hchildren.length < 15 then
+                       var lst = hdescendants.to_a
+                       sorter.sort lst
+                       section.add_child tpl_list("descendants", "Descendants", lst)
+               end
+
+               parent.add_child section
+       end
+
+       private fun tpl_list(id: String, title: String, elts: Array[MClass]): TplArticle do
+               var article = new TplArticle.with_title(id, title)
+               var list = new TplList.with_classes(["list-unstyled", "list-definition"])
+               for elt in elts do list.elts.add elt.tpl_list_item
+               article.content = list
+               return article
+       end
+
+       private fun tpl_properties(parent: TplSection) do
+               var mod_map = sort_by_mmodule(mprops2mdefs.keys)
+               var owner_map = sort_by_public_owner(mod_map.keys)
+               var owners = owner_map.keys.to_a
+
+               for owner in owners do
+                       var section = new TplSection(owner.nitdoc_anchor)
+                       var title = new Template
+                       title.add "Introductions in "
+                       title.add owner.tpl_link
+                       section.title = title
+                       section.summary_title = "In {owner.nitdoc_name}"
+                       for mmodule in owner_map[owner] do
+                               # properties
+                               var mprops = mod_map[mmodule]
+                               var sorter = new MPropertyNameSorter
+                               var kind_map = sort_by_kind(mprops)
+
+                               # virtual types
+                               var elts = kind_map["type"].to_a
+                               sorter.sort(elts)
+                               for elt in elts do
+                                       var defs = mprops2mdefs[elt].to_a
+                                       section.add_child tpl_mprop_article(elt, defs)
+                               end
+
+                               # constructors
+                               elts = kind_map["init"].to_a
+                               sorter.sort(elts)
+                               for elt in elts do
+                                       var defs = mprops2mdefs[elt].to_a
+                                       section.add_child tpl_mprop_article(elt, defs)
+                               end
+
+                               # methods
+                               elts = kind_map["fun"].to_a
+                               sorter.sort(elts)
+                               for elt in elts do
+                                       var defs = mprops2mdefs[elt].to_a
+                                       section.add_child tpl_mprop_article(elt, defs)
+                               end
+                       end
+                       parent.add_child section
+               end
+       end
+
+       redef fun tpl_content do
+               tpl_sidebar_properties
+               var top = tpl_intro
+               tpl_concerns(top)
+               tpl_inheritance(top)
+               tpl_properties(top)
+               tpl_page.add_section top
+       end
+
+       private fun sort_by_mproperty(mpropdefs: Collection[MPropDef]): Map[MProperty, Set[MPropDef]] do
+               var map = new HashMap[MProperty, Set[MPropDef]]
+               for mpropdef in mpropdefs do
+                       var mproperty = mpropdef.mproperty
+                       if not map.has_key(mproperty) then map[mproperty] = new HashSet[MPropDef]
+                       map[mproperty].add mpropdef
+               end
+               return map
+       end
+
+       private fun sort_by_mmodule(mprops: Collection[MProperty]): Map[MModule, Set[MProperty]] do
+               var map = new HashMap[MModule, Set[MProperty]]
+               for mprop in mprops do
+                       var mpropdefs = mprops2mdefs[mprop].to_a
+                       ctx.mainmodule.linearize_mpropdefs(mpropdefs)
+                       var mmodule = mpropdefs.first.mclassdef.mmodule
+                       if not map.has_key(mmodule) then map[mmodule] = new HashSet[MProperty]
+                       map[mmodule].add mprop
+               end
+               return map
+       end
+
+       private fun sort_by_kind(mprops: Collection[MProperty]): Map[String, Set[MProperty]] do
+               var map = new HashMap[String, Set[MProperty]]
+               map["type"] = new HashSet[MProperty]
+               map["init"] = new HashSet[MProperty]
+               map["fun"] = new HashSet[MProperty]
+               for mprop in mprops do
+                       if mprop isa MVirtualTypeProp then
+                               map["type"].add mprop
+                       else if mprop isa MMethod then
+                               if mprop.is_init then
+                                       map["init"].add mprop
+                               else
+                                       map["fun"].add mprop
+                               end
+                       end
+               end
+               return map
+       end
+
+       private fun mclass_inherited_mprops: Set[MProperty] do
+               var res = new HashSet[MProperty]
+               var local = mclass.local_mproperties(ctx.min_visibility)
+               for mprop in mclass.inherited_mproperties(ctx.mainmodule, ctx.min_visibility) do
+                       if local.has(mprop) then continue
+                       #if mprop isa MMethod and mprop.is_init then continue
+                       if mprop.intro.mclassdef.mclass.name == "Object" and
+                               (mprop.visibility == protected_visibility or
+                               mprop.intro.mclassdef.mmodule.name != "kernel") then continue
+                       res.add mprop
+               end
+               res.add_all local
+               return res
+       end
+
+       private fun collect_mmodules(mprops: Collection[MProperty]): Set[MModule] do
+               var res = new HashSet[MModule]
+               for mprop in mprops do
+                       if mprops2mdefs.has_key(mprop) then
+                               for mpropdef in mprops2mdefs[mprop] do res.add mpropdef.mclassdef.mmodule
+                       end
+               end
+               return res
+       end
+
+       private fun sort_by_public_owner(mmodules: Collection[MModule]): Map[MModule, Set[MModule]] do
+               var map = new HashMap[MModule, Set[MModule]]
+               for mmodule in mmodules do
+                       var owner = mmodule
+                       if mmodule.public_owner != null then owner = mmodule.public_owner.as(not null)
+                       if not map.has_key(owner) then map[owner] = new HashSet[MModule]
+                       map[owner].add mmodule
+               end
+               return map
+       end
+
+       # Generate dot hierarchy for classes
+       fun tpl_dot(mclasses: Collection[MClass]): nullable TplArticle do
+               var poset = new POSet[MClass]
+
+               for mclass in mclasses do
+                       poset.add_node mclass
+                       for oclass in mclasses do
+                               poset.add_node oclass
+                               if mclass.in_hierarchy(ctx.mainmodule) < oclass then
+                                       poset.add_edge(mclass, oclass)
+                               end
+                       end
+               end
+
+               var op = new FlatBuffer
+               var name = "dep_{mclass.name}"
+               op.append("digraph {name} \{ rankdir=BT; node[shape=none,margin=0,width=0,height=0,fontsize=10]; edge[dir=none,color=gray]; ranksep=0.2; nodesep=0.1;\n")
+               for c in poset do
+                       if c == mclass then
+                               op.append("\"{c.name}\"[shape=box,margin=0.03];\n")
+                       else
+                               op.append("\"{c.name}\"[URL=\"{c.nitdoc_url}\"];\n")
+                       end
+                       for c2 in poset[c].direct_greaters do
+                               op.append("\"{c.name}\"->\"{c2.name}\";\n")
+                       end
+               end
+               op.append("\}\n")
+               return tpl_graph(op, name, "Inheritance graph")
+       end
+end
+