nitdoc: Escape links’ attributes.
[nit.git] / src / doc / doc_pages.nit
index 2d2c733..0d26ad6 100644 (file)
 # Nitdoc page generation
 module doc_pages
 
+import toolcontext
 import doc_model
+private import json::static
 
-# The NitdocContext contains all the knowledge used for doc generation
-class NitdocContext
+redef class ToolContext
        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_no_attributes = new OptionBool("ignore the attributes",
+                       "--no-attributes")
        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")
 
@@ -38,15 +41,15 @@ class NitdocContext
        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)
+       redef init do
+               super
+
+               var opts = option_context
+               opts.add_option(opt_dir, opt_source, opt_sharedir, opt_shareurl,
+                               opt_no_attributes, opt_nodot, opt_private)
                opts.add_option(opt_custom_title, opt_custom_footer, opt_custom_intro, opt_custom_brand)
                opts.add_option(opt_github_upstream, opt_github_base_sha1, opt_github_gitdir)
                opts.add_option(opt_piwik_tracker, opt_piwik_site_id)
@@ -54,19 +57,25 @@ class NitdocContext
                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)
+               tooldescription = tpl.write_to_string
        end
 
-       private fun process_options do
+       redef fun process_options(args) do
+               super
+
+               # output dir
+               var output_dir = opt_dir.value
+               if output_dir == null then
+                       output_dir = "doc"
+               end
+               self.output_dir = output_dir
+               # min visibility
                if opt_private.value then
                        min_visibility = none_visibility
                else
                        min_visibility = protected_visibility
                end
+               # github urls
                var gh_upstream = opt_github_upstream.value
                var gh_base_sha = opt_github_base_sha1.value
                var gh_gitdir = opt_github_gitdir.value
@@ -78,90 +87,111 @@ class NitdocContext
                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
+       # Filter the entity based on the options specified by the user.
+       #
+       # Return `true` if the specified entity has to be included in the generated
+       # documentation
+       private fun filter_mclass(mclass: MClass): Bool do
+               return mclass.visibility >= min_visibility
+       end
+
+       # Filter the entity based on the options specified by the user.
+       #
+       # Return `true` if the specified entity has to be included in the generated
+       # documentation
+       private fun filter_mproperty(mproperty: MProperty): Bool do
+               return mproperty.visibility >= min_visibility and
+                       not (opt_no_attributes.value and mproperty isa MAttribute)
        end
+end
+
+# The Nitdoc class explores the model and generate pages for each mentities found
+class Nitdoc
+       var ctx: ToolContext
+       var model: Model
+       var mainmodule: MModule
 
-       fun generate_nitdoc do
+       fun generate do
                init_output_dir
                overview
                search
+               groups
                modules
                classes
+               properties
                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
+               var output_dir = ctx.output_dir
                if not output_dir.file_exists then output_dir.mkdir
                # locate share dir
-               var sharedir = opt_sharedir.value
+               var sharedir = ctx.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"
+                       var dir = ctx.nit_dir
+                       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}/")
+               if ctx.opt_shareurl.value == null then
+                       sys.system("cp -r -- {sharedir.to_s.escape_to_sh}/* {output_dir.to_s.escape_to_sh}/")
                else
-                       sys.system("cp -r {sharedir.to_s}/resources/ {output_dir.to_s}/resources/")
+                       sys.system("cp -r -- {sharedir.to_s.escape_to_sh}/resources/ {output_dir.to_s.escape_to_sh}/resources/")
                end
 
        end
 
        private fun overview do
-               var overviewpage = new NitdocOverview(self)
-               overviewpage.render.write_to_file("{output_dir.to_s}/index.html")
+               var page = new NitdocOverview(ctx, model, mainmodule)
+               page.render.write_to_file("{ctx.output_dir.to_s}/{page.page_url}")
        end
 
        private fun search do
-               var searchpage = new NitdocSearch(self)
-               searchpage.render.write_to_file("{output_dir.to_s}/search.html")
+               var page = new NitdocSearch(ctx, model, mainmodule)
+               page.render.write_to_file("{ctx.output_dir.to_s}/{page.page_url}")
+       end
+
+       private fun groups do
+               for mproject in model.mprojects do
+                       for mgroup in mproject.mgroups.to_a do
+                               var page = new NitdocGroup(ctx, model, mainmodule, mgroup)
+                               page.render.write_to_file("{ctx.output_dir.to_s}/{page.page_url}")
+                       end
+               end
        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}")
+               for mmodule in model.mmodules do
+                       if mmodule.is_fictive then continue
+                       var page = new NitdocModule(ctx, model, mainmodule, mmodule)
+                       page.render.write_to_file("{ctx.output_dir.to_s}/{page.page_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}")
+               for mclass in model.mclasses do
+                       if not ctx.filter_mclass(mclass) then continue
+                       var page = new NitdocClass(ctx, model, mainmodule, mclass)
+                       page.render.write_to_file("{ctx.output_dir.to_s}/{page.page_url}")
+               end
+       end
+
+       private fun properties do
+               for mproperty in model.mproperties do
+                       if not ctx.filter_mproperty(mproperty) then continue
+                       if mproperty isa MInnerClass then continue
+                       var page = new NitdocProperty(ctx, model, mainmodule, mproperty)
+                       page.render.write_to_file("{ctx.output_dir.to_s}/{page.page_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")
+               var quicksearch = new QuickSearch(ctx, model)
+               quicksearch.render.write_to_file("{ctx.output_dir.to_s}/quicksearch-list.js")
        end
 end
 
@@ -174,69 +204,88 @@ end
 # 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]]
+       private var table = new QuickSearchTable
+
+       var ctx: ToolContext
+       var model: Model
 
-       init(ctx: NitdocContext) do
-               for mmodule in ctx.mbuilder.model.mmodules do
-                       if mmodule.name == "<main>" then continue
-                       mmodules.add mmodule
+       init do
+               for mmodule in model.mmodules do
+                       if mmodule.is_fictive then continue
+                       add_result_for(mmodule.name, mmodule.full_name, mmodule.nitdoc_url)
                end
-               for mclass in ctx.mbuilder.model.mclasses do
-                       if mclass.visibility < ctx.min_visibility then continue
-                       mclasses.add mclass
+               for mclass in model.mclasses do
+                       if not ctx.filter_mclass(mclass) then continue
+                       add_result_for(mclass.name, mclass.full_name, mclass.nitdoc_url)
                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]
+               for mproperty in model.mproperties do
+                       if not ctx.filter_mproperty(mproperty) then continue
+                       for mpropdef in mproperty.mpropdefs do
+                               var full_name = mpropdef.mclassdef.mclass.full_name
+                               var cls_url = mpropdef.mclassdef.mclass.nitdoc_url
+                               var def_url = "{cls_url}#{mpropdef.mproperty.nitdoc_id}"
+                               add_result_for(mproperty.name, full_name, def_url)
                        end
-                       mpropdefs[mproperty.name].add_all(mproperty.mpropdefs)
                end
        end
 
+       private fun add_result_for(query: String, txt: String, url: String) do
+               table[query].add new QuickSearchResult(txt, url)
+       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 " \};"
+               var buffer = new RopeBuffer
+               tpl.add buffer
+               buffer.append "var nitdocQuickSearchRawList="
+               table.append_json buffer
+               buffer.append ";"
                return tpl
        end
 end
 
+# The result map for QuickSearch.
+private class QuickSearchTable
+       super JsonMapRead[String, QuickSearchResultList]
+       super HashMap[String, QuickSearchResultList]
+
+       redef fun provide_default_value(key) do
+               var v = new QuickSearchResultList
+               self[key] = v
+               return v
+       end
+end
+
+# A QuickSearch result list.
+private class QuickSearchResultList
+       super JsonSequenceRead[QuickSearchResult]
+       super Array[QuickSearchResult]
+end
+
+# A QuickSearch result.
+private class QuickSearchResult
+       super Jsonable
+
+       # The text of the link.
+       var txt: String
+
+       # The destination of the link.
+       var url: String
+
+       redef fun to_json do
+               return "\{\"txt\":{txt.to_json},\"url\":{url.to_json}\}"
+       end
+end
+
 # Nitdoc base page
 # Define page structure and properties
 abstract class NitdocPage
 
-       private var ctx: NitdocContext
+       private var ctx: ToolContext
        private var model: Model
+       private var mainmodule: MModule
        private var name_sorter = new MEntityNameSorter
 
-       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 = "."
@@ -247,6 +296,7 @@ abstract class NitdocPage
                # build page
                var tpl = tpl_page
                tpl.title = tpl_title
+               tpl.url = page_url
                tpl.shareurl = shareurl
                tpl.topmenu = tpl_topmenu
                tpl_content
@@ -263,6 +313,9 @@ abstract class NitdocPage
                return tpl
        end
 
+       # URL to this page.
+       fun page_url: String is abstract
+
        # Build page template
        fun tpl_page: TplPage is abstract
 
@@ -279,7 +332,7 @@ abstract class NitdocPage
 
        # Build top menu template
        fun tpl_topmenu: TplTopMenu do
-               var topmenu = new TplTopMenu
+               var topmenu = new TplTopMenu(page_url)
                var brand = ctx.opt_custom_brand.value
                if brand != null then
                        var tpl = new Template
@@ -288,6 +341,8 @@ abstract class NitdocPage
                        tpl.add "</span>"
                        topmenu.brand = tpl
                end
+               topmenu.add_link new TplLink("index.html", "Overview")
+               topmenu.add_link new TplLink("search.html", "Index")
                return topmenu
        end
 
@@ -296,20 +351,29 @@ abstract class NitdocPage
 
        # 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
+       fun tpl_graph(dot: Buffer, name: String, title: nullable 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")
+               var path = output_dir / name
+               var path_sh = path.escape_to_sh
+               var file = new OFStream.open("{path}.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")
+               sys.system("\{ test -f {path_sh}.png && test -f {path_sh}.s.dot && diff -- {path_sh}.dot {path_sh}.s.dot >/dev/null 2>&1 ; \} || \{ cp -- {path_sh}.dot {path_sh}.s.dot && dot -Tpng -o{path_sh}.png -Tcmapx -o{path_sh}.map {path_sh}.s.dot ; \}")
+               var fmap = new IFStream.open("{path}.map")
                var map = fmap.read_all
                fmap.close
 
-               var article = new TplArticle.with_title("graph", title)
+               var article = new TplArticle("graph")
+               var alt = ""
+               if title != null then
+                       article.title = title
+                       alt = "alt='{title.html_escape}'"
+               end
+               article.css_classes.add "text-center"
                var content = new Template
-               content.add "<img src='{name}.png' usemap='#{name}' style='margin:auto' alt='{title}'/>"
+               var name_html = name.html_escape
+               content.add "<img src='{name_html}.png' usemap='#{name_html}' style='margin:auto' {alt}/>"
                content.add map
                article.content = content
                return article
@@ -320,7 +384,10 @@ abstract class NitdocPage
        do
                if location == null then return null
                var source = ctx.opt_source.value
-               if source == null then return "({location.file.filename.simplify_path})"
+               if source == null then
+                       var url = location.file.filename.simplify_path
+                       return "<a target='_blank' title='Show source' href=\"{url.html_escape}\">View Source</a>"
+               end
                # THIS IS JUST UGLY ! (but there is no replace yet)
                var x = source.split_with("%f")
                source = x.join(location.file.filename.simplify_path)
@@ -329,105 +396,178 @@ abstract class NitdocPage
                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>)"
+               return "<a target='_blank' title='Show source' href=\"{source.to_s.html_escape}\">View Source</a>"
+       end
+
+       # MProject description template
+       fun tpl_mproject_article(mproject: MProject): TplArticle do
+               var article = mproject.tpl_article
+               article.subtitle = mproject.tpl_declaration
+               article.content = mproject.tpl_definition
+               var mdoc = mproject.mdoc_or_fallback
+               if mdoc != null then
+                       article.content = mdoc.tpl_short_comment
+               end
+               return article
+       end
+
+       # MGroup description template
+       fun tpl_mgroup_article(mgroup: MGroup): TplArticle do
+               var article = mgroup.tpl_article
+               article.subtitle = mgroup.tpl_declaration
+               article.content = mgroup.tpl_definition
+               return article
+       end
+
+       # MModule description template
+       fun tpl_mmodule_article(mmodule: MModule): TplArticle do
+               var article = mmodule.tpl_article
+               article.subtitle = mmodule.tpl_declaration
+               article.content = mmodule.tpl_definition
+               # mclassdefs list
+               var intros = mmodule.intro_mclassdefs(ctx.min_visibility).to_a
+               if not intros.is_empty then
+                       mainmodule.linearize_mclassdefs(intros)
+                       var intros_art = new TplArticle.with_title("{mmodule.nitdoc_id}.intros", "Introduces")
+                       var intros_lst = new TplList.with_classes(["list-unstyled", "list-labeled"])
+                       for mclassdef in intros do
+                               intros_lst.add_li mclassdef.tpl_list_item
+                       end
+                       if not intros_lst.is_empty then
+                               intros_art.content = intros_lst
+                               article.add_child intros_art
+                       end
+               end
+               var redefs = mmodule.redef_mclassdefs(ctx.min_visibility).to_a
+               if not redefs.is_empty then
+                       mainmodule.linearize_mclassdefs(redefs)
+                       var redefs_art = new TplArticle.with_title("{mmodule.nitdoc_id}.redefs", "Redefines")
+                       var redefs_lst = new TplList.with_classes(["list-unstyled", "list-labeled"])
+                       for mclassdef in redefs do
+                               redefs_lst.add_li mclassdef.tpl_list_item
+                       end
+                       if not redefs_lst.is_empty then
+                               redefs_art.content = redefs_lst
+                               article.add_child redefs_art
+                       end
+               end
+               return article
        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
-
+               var article = mclass.tpl_article
                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)
+                       var intro_article = mclass.intro.tpl_short_article
+                       intro_article.source_link = tpl_showsource(mclass.intro.location)
+                       article.add_child intro_article
+               end
+               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
+                       var redef_article = mclassdef.tpl_article
+                       redef_article.source_link = tpl_showsource(mclassdef.location)
+                       article.add_child redef_article
+                       # mpropdefs list
+                       var intros = new TplArticle.with_title("{mclassdef.nitdoc_id}.intros", "Introduces")
+                       var intros_lst = new TplList.with_classes(["list-unstyled", "list-labeled"])
+                       for mpropdef in mclassdef.collect_intro_mpropdefs(ctx.min_visibility) do
+                               intros_lst.add_li mpropdef.tpl_list_item
+                       end
+                       if not intros_lst.is_empty then
+                               intros.content = intros_lst
+                               redef_article.add_child intros
+                       end
+                       var redefs = new TplArticle.with_title("{mclassdef.nitdoc_id}.redefs", "Redefines")
+                       var redefs_lst = new TplList.with_classes(["list-unstyled", "list-labeled"])
+                       for mpropdef in mclassdef.collect_redef_mpropdefs(ctx.min_visibility) do
+                               redefs_lst.add_li mpropdef.tpl_list_item
+                       end
+                       if not redefs_lst.is_empty then
+                               redefs.content = redefs_lst
+                               redef_article.add_child redefs
                        end
-                       content.add prop_def
                end
-               article.content = content
+               return article
+       end
+
+       # MClassDef description template
+       fun tpl_mclassdef_article(mclassdef: MClassDef): TplArticle do
+               var article = mclassdef.tpl_article
+               if mclassdef.is_intro then article.content = null
+               article.source_link = tpl_showsource(mclassdef.location)
                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)
+       #
+       # `main_mpropdef`: The most important mpropdef to display
+       # `local_mpropdefs`: List of other locally defined mpropdefs to display
+       # `lin`: full linearization from local_mpropdefs to intro (displayed in redef tree)
+       fun tpl_mprop_article(main_mpropdef: MPropDef, local_mpropdefs: Array[MPropDef],
+          lin: Array[MPropDef]): TplArticle do
+               var mprop = main_mpropdef.mproperty
+               var article = new TplArticle(mprop.nitdoc_id)
                var title = new Template
-               title.add icon
-               title.add mprop.nitdoc_name
-               title.add mprop.intro.tpl_signature
+               title.add mprop.tpl_icon
+               title.add "<span id='{main_mpropdef.nitdoc_id}'></span>"
+               if main_mpropdef.is_intro then
+                       title.add mprop.tpl_link
+                       title.add mprop.intro.tpl_signature
+               else
+                       var cls_url = mprop.intro.mclassdef.mclass.nitdoc_url
+                       var def_url = "{cls_url}#{mprop.nitdoc_id}"
+                       var lnk = new TplLink.with_title(def_url, mprop.name, "Go to introduction")
+                       title.add "redef "
+                       title.add lnk
+               end
                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
+               article.summary_title = "{mprop.nitdoc_name}"
+               article.subtitle = main_mpropdef.tpl_namespace
+               if main_mpropdef.mdoc_or_fallback != null then
+                       article.content = main_mpropdef.mdoc_or_fallback.tpl_comment
                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
+               var subarticle = new TplArticle("{main_mpropdef.nitdoc_id}.redefs")
+               # Add redef in same `MClass`
+               if local_mpropdefs.length > 1 then
+                       for mpropdef in local_mpropdefs do
+                               if mpropdef == main_mpropdef then continue
+                               var redef_article = new TplArticle("{mpropdef.nitdoc_id}")
+                               var redef_title = new Template
+                               redef_title.add "also redef in "
+                               redef_title.add mpropdef.tpl_namespace
+                               redef_article.title = redef_title
+                               redef_article.title_classes.add "signature info"
+                               redef_article.css_classes.add "nospace"
+                               var redef_content = new Template
+                               if mpropdef.mdoc_or_fallback != null then
+                                       redef_content.add mpropdef.mdoc_or_fallback.tpl_comment
+                               end
+                               redef_article.content = redef_content
+                               subarticle.add_child redef_article
+                       end
                end
-               article.content = content
+               # Add linearization
+               if lin.length > 1 then
+                       var lin_article = new TplArticle("{main_mpropdef.nitdoc_id}.lin")
+                       lin_article.title = "Inheritance"
+                       var lst = new TplList.with_classes(["list-unstyled", "list-labeled"])
+                       for mpropdef in lin do
+                               lst.add_li mpropdef.tpl_inheritance_item
+                       end
+                       lin_article.content = lst
+                       subarticle.add_child lin_article
+               end
+               article.add_child subarticle
+               return article
+       end
+
+       # MProperty description template
+       fun tpl_mpropdef_article(mpropdef: MPropDef): TplArticle do
+               var article = mpropdef.tpl_article
+               article.source_link = tpl_showsource(mpropdef.location)
                return article
        end
 end
@@ -437,8 +577,6 @@ end
 class NitdocOverview
        super NitdocPage
 
-       init(ctx: NitdocContext) do super(ctx)
-
        private var page = new TplPage
        redef fun tpl_page do return page
 
@@ -453,12 +591,7 @@ class NitdocOverview
                end
        end
 
-       redef fun tpl_topmenu do
-               var topmenu = super
-               topmenu.add_item(new TplLink("#", "Overview"), true)
-               topmenu.add_item(new TplLink("search.html", "Index"), false)
-               return topmenu
-       end
+       redef fun page_url do return "index.html"
 
        # intro text
        private fun tpl_intro: TplSection do
@@ -474,9 +607,12 @@ class NitdocOverview
        # projects list
        private fun tpl_projects(section: TplSection) do
                # Projects list
+               var mprojects = model.mprojects.to_a
+               var sorter = new MConcernRankSorter
+               sorter.sort mprojects
                var ssection = new TplSection.with_title("projects", "Projects")
-               for mproject in model.mprojects do
-                       ssection.add_child mproject.tpl_article
+               for mproject in mprojects do
+                       ssection.add_child tpl_mproject_article(mproject)
                end
                section.add_child ssection
        end
@@ -493,19 +629,12 @@ end
 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_item(new TplLink("index.html", "Overview"), false)
-               topmenu.add_item(new TplLink("#", "Index"), true)
-               return topmenu
-       end
+       redef fun page_url do return "search.html"
 
        redef fun tpl_content do
                var tpl = new TplSearchPage("search_all")
@@ -536,8 +665,8 @@ class NitdocSearch
        # 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
+               for mmodule in model.mmodule_importation_hierarchy do
+                       if mmodule.is_fictive then continue
                        sorted.add mmodule
                end
                name_sorter.sort(sorted)
@@ -547,8 +676,8 @@ class NitdocSearch
        # 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
+               for mclass in model.mclasses do
+                       if not ctx.filter_mclass(mclass) then continue
                        sorted.add mclass
                end
                name_sorter.sort(sorted)
@@ -558,26 +687,166 @@ class NitdocSearch
        # 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
+               for mproperty in model.mproperties do
+                       if ctx.filter_mproperty(mproperty) then sorted.add mproperty
                end
                name_sorter.sort(sorted)
                return sorted
        end
 end
 
+# A group page
+# Display a flattened view of the group
+class NitdocGroup
+       super NitdocPage
+
+       private var mgroup: MGroup
+
+       private var concerns: ConcernsTree is noinit
+       private var intros: Set[MClass] is noinit
+       private var redefs: Set[MClass] is noinit
+
+       init do
+               self.concerns = model.concerns_tree(mgroup.collect_mmodules)
+               self.concerns.sort_with(new MConcernRankSorter)
+               self.intros = mgroup.in_nesting_intro_mclasses(ctx.min_visibility)
+               var redefs = new HashSet[MClass]
+               for rdef in mgroup.in_nesting_redef_mclasses(ctx.min_visibility) do
+                       if intros.has(rdef) then continue
+                       redefs.add rdef
+               end
+               self.redefs = redefs
+       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 mgroup.nitdoc_name
+
+       redef fun page_url do return mgroup.nitdoc_url
+
+       redef fun tpl_topmenu do
+               var topmenu = super
+               var mproject = mgroup.mproject
+               if not mgroup.is_root then
+                       topmenu.add_link new TplLink(mproject.nitdoc_url, mproject.nitdoc_name)
+               end
+               topmenu.add_link new TplLink(page_url, mproject.nitdoc_name)
+               return topmenu
+       end
+
+       # Class list to display in sidebar
+       fun tpl_sidebar_mclasses do
+               var mclasses = new HashSet[MClass]
+               mclasses.add_all intros
+               mclasses.add_all redefs
+               if mclasses.is_empty then return
+               var list = new TplList.with_classes(["list-unstyled", "list-labeled"])
+
+               var sorted = mclasses.to_a
+               name_sorter.sort(sorted)
+               for mclass in sorted do
+                       list.add_li tpl_sidebar_item(mclass)
+               end
+               tpl_sidebar.boxes.add new TplSideBox.with_content("All classes", list)
+       end
+
+       private fun tpl_sidebar_item(def: MClass): TplListItem do
+               var classes = def.intro.tpl_css_classes.to_a
+               if intros.has(def) then
+                       classes.add "intro"
+               else
+                       classes.add "redef"
+               end
+               var lnk = new Template
+               lnk.add new TplLabel.with_classes(classes)
+               lnk.add def.tpl_link
+               return new TplListItem.with_content(lnk)
+       end
+
+       # intro text
+       private fun tpl_intro: TplSection do
+               var section = new TplSection.with_title("top", tpl_title)
+               var article = new TplArticle("intro")
+
+               if mgroup.is_root then
+                       section.subtitle = mgroup.mproject.tpl_declaration
+                       article.content = mgroup.mproject.tpl_definition
+               else
+                       section.subtitle = mgroup.tpl_declaration
+                       article.content = mgroup.tpl_definition
+               end
+               section.add_child article
+               return section
+       end
+
+       private fun tpl_concerns(section: TplSection) do
+               if concerns.is_empty then return
+               section.add_child new TplArticle.with_content("concerns", "Concerns", concerns.to_tpl)
+       end
+
+       private fun tpl_groups(parent: TplSection) do
+               var lst = concerns.to_a
+               var section = parent
+               for mentity in lst do
+                       if mentity isa MProject then
+                               section.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MGroup then
+                               section.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MModule then
+                               section.add_child tpl_mmodule_article(mentity)
+                       end
+               end
+       end
+
+       redef fun tpl_content do
+               tpl_sidebar_mclasses
+               var top = tpl_intro
+               tpl_concerns(top)
+               tpl_groups(top)
+               tpl_page.add_section top
+       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 module page
 # Display the list of introduced and redefined classes in module
 class NitdocModule
        super NitdocPage
 
        private var mmodule: MModule
+       private var concerns: ConcernsTree is noinit
+       private var mclasses2mdefs: Map[MClass, Set[MClassDef]] is noinit
+       private var mmodules2mclasses: Map[MModule, Set[MClass]] is noinit
 
-       init(mmodule: MModule, ctx: NitdocContext) do
-               self.mmodule = mmodule
-               super(ctx)
+
+       init do
+               var mclassdefs = new HashSet[MClassDef]
+               mclassdefs.add_all mmodule.intro_mclassdefs(ctx.min_visibility)
+               mclassdefs.add_all mmodule.redef_mclassdefs(ctx.min_visibility)
+               self.mclasses2mdefs = sort_by_mclass(mclassdefs)
+               self.mmodules2mclasses = group_by_mmodule(mclasses2mdefs.keys)
+               self.concerns = model.concerns_tree(mmodules2mclasses.keys)
+               # rank concerns
+               mmodule.mgroup.mproject.booster_rank = -1000
+               mmodule.mgroup.booster_rank = -1000
+               mmodule.booster_rank = -1000
+               self.concerns.sort_with(new MConcernRankSorter)
+               mmodule.mgroup.mproject.booster_rank = 0
+               mmodule.mgroup.booster_rank = 0
+               mmodule.booster_rank = 0
        end
 
        private var page = new TplPage
@@ -586,25 +855,55 @@ class NitdocModule
        private var sidebar = new TplSidebar
        redef fun tpl_sidebar do return sidebar
 
-       redef fun tpl_title do return "{mmodule.nitdoc_name}"
+       redef fun tpl_title do return mmodule.nitdoc_name
+       redef fun page_url do return mmodule.nitdoc_url
 
        redef fun tpl_topmenu do
                var topmenu = super
-               topmenu.add_item(new TplLink("index.html", "Overview"), false)
-               topmenu.add_item(new TplLink("#", "{mmodule.nitdoc_name}"), true)
-               topmenu.add_item(new TplLink("search.html", "Index"), false)
+               var mproject = mmodule.mgroup.mproject
+               topmenu.add_link new TplLink(mproject.nitdoc_url, mproject.nitdoc_name)
+               topmenu.add_link new TplLink(page_url, mmodule.nitdoc_name)
                return topmenu
        end
 
+       # Class list to display in sidebar
+       fun tpl_sidebar_mclasses do
+               var mclasses = new HashSet[MClass]
+               mclasses.add_all mmodule.filter_intro_mclasses(ctx.min_visibility)
+               mclasses.add_all mmodule.filter_redef_mclasses(ctx.min_visibility)
+               if mclasses.is_empty then return
+               var list = new TplList.with_classes(["list-unstyled", "list-labeled"])
+
+               var sorted = mclasses.to_a
+               name_sorter.sort(sorted)
+               for mclass in sorted do
+                       list.add_li tpl_sidebar_item(mclass)
+               end
+               tpl_sidebar.boxes.add new TplSideBox.with_content("All classes", list)
+       end
+
+       private fun tpl_sidebar_item(def: MClass): TplListItem do
+               var classes = def.intro.tpl_css_classes.to_a
+               if def.intro_mmodule == mmodule then
+                       classes.add "intro"
+               else
+                       classes.add "redef"
+               end
+               var lnk = new Template
+               lnk.add new TplLabel.with_classes(classes)
+               lnk.add def.tpl_link
+               return new TplListItem.with_content(lnk)
+       end
+
        # intro text
        private fun tpl_intro: TplSection do
-               var section = new TplSection.with_title(mmodule.nitdoc_anchor, tpl_title)
+               var section = new TplSection.with_title("top", 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.source_link = tpl_showsource(location)
                article.content = def
                section.add_child article
                return section
@@ -613,31 +912,23 @@ class NitdocModule
        # 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 section = new TplSection.with_title("inheritance", "Inheritance")
+               var section = new TplSection.with_title("dependencies", "Dependencies")
 
                # Graph
                var mmodules = new HashSet[MModule]
-               mmodules.add_all nested
+               mmodules.add_all mmodule.nested_mmodules
                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
-                       name_sorter.sort lst
-                       section.add_child tpl_list("nesting", "Nested modules", lst)
-               end
-
                # Imports
                var lst = new Array[MModule]
                for dep in imports do
@@ -673,40 +964,57 @@ class NitdocModule
                return article
        end
 
+       private fun tpl_concerns(parent: TplSection) do
+               if concerns.is_empty then return
+               parent.add_child new TplArticle.with_content("concerns", "Concerns", concerns.to_tpl)
+       end
+
        private fun tpl_mclasses(parent: TplSection) do
-               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
-               name_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
-               name_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)
+               for mentity in concerns do
+                       if mentity isa MProject then
+                               parent.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MGroup then
+                               parent.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MModule then
+                               var section = new TplSection(mentity.nitdoc_id)
+                               var title = new Template
+                               if mentity == mmodule then
+                                       title.add "in "
+                                       section.summary_title = "in {mentity.nitdoc_name}"
+                               else
+                                       title.add "from "
+                                       section.summary_title = "from {mentity.nitdoc_name}"
+                               end
+                               title.add mentity.tpl_namespace
+                               section.title = title
+
+                               var mclasses = mmodules2mclasses[mentity].to_a
+                               name_sorter.sort(mclasses)
+                               for mclass in mclasses do
+                                       section.add_child tpl_mclass_article(mclass, mclasses2mdefs[mclass].to_a)
+                               end
+                               parent.add_child section
+                       end
                end
-               parent.add_child section
+       end
 
-               # redefs
-               section = new TplSection.with_title("redefs", "Refinements")
-               var redefs = mmodule.in_nesting_redef_mclasses(ctx.min_visibility).to_a
-               name_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)
+       private fun group_by_mmodule(mclasses: Collection[MClass]): Map[MModule, Set[MClass]] do
+               var res = new HashMap[MModule, Set[MClass]]
+               for mclass in mclasses do
+                       var mmodule = mclass.intro_mmodule
+                       if not res.has_key(mmodule) then
+                               res[mmodule] = new HashSet[MClass]
+                       end
+                       res[mmodule].add(mclass)
                end
-               parent.add_child section
+               return res
        end
 
        redef fun tpl_content do
+               tpl_sidebar_mclasses
                var top = tpl_intro
                tpl_inheritance(top)
+               tpl_concerns(top)
                tpl_mclasses(top)
                tpl_page.add_section top
        end
@@ -726,21 +1034,21 @@ class NitdocModule
                        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")
+               var op = new RopeBuffer
+               var name = "dep_module_{mmodule.nitdoc_id}"
+               op.append("digraph \"{name.escape_to_dot}\" \{ 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")
+                               op.append("\"{mmodule.name.escape_to_dot}\"[shape=box,margin=0.03];\n")
                        else
-                               op.append("\"{mmodule.name}\"[URL=\"{mmodule.nitdoc_url}\"];\n")
+                               op.append("\"{mmodule.name.escape_to_dot}\"[URL=\"{mmodule.nitdoc_url.escape_to_dot}\"];\n")
                        end
                        for omodule in poset[mmodule].direct_greaters do
-                               op.append("\"{mmodule.name}\"->\"{omodule.name}\";\n")
+                               op.append("\"{mmodule.name.escape_to_dot}\"->\"{omodule.name.escape_to_dot}\";\n")
                        end
                end
                op.append("\}\n")
-               return tpl_graph(op, name, "Dependency graph")
+               return tpl_graph(op, name, null)
        end
 
        private fun sort_by_mclass(mclassdefs: Collection[MClassDef]): Map[MClass, Set[MClassDef]] do
@@ -760,15 +1068,18 @@ class NitdocClass
        super NitdocPage
 
        private var mclass: MClass
-       private var mprops2mdefs: Map[MProperty, Set[MPropDef]]
+       private var concerns: ConcernsTree is noinit
+       private var mprops2mdefs: Map[MProperty, Set[MPropDef]] is noinit
+       private var mmodules2mprops: Map[MModule, Set[MProperty]] is noinit
 
-       init(mclass: MClass, ctx: NitdocContext) do
-               self.mclass = mclass
-               super(ctx)
+       init do
                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)
+               self.mprops2mdefs = sort_by_mproperty(mpropdefs)
+               self.mmodules2mprops = sort_by_mmodule(mprops2mdefs.keys)
+               self.concerns = model.concerns_tree(mmodules2mprops.keys)
+               self.concerns.sort_with(new MConcernRankSorter)
        end
 
        private var page = new TplPage
@@ -778,37 +1089,29 @@ class NitdocClass
        redef fun tpl_sidebar do return sidebar
 
        redef fun tpl_title do return "{mclass.nitdoc_name}{mclass.tpl_signature.write_to_string}"
+       redef fun page_url do return mclass.nitdoc_url
 
        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_item(new TplLink("index.html", "Overview"), false)
-               topmenu.add_item(new TplLink("{mmodule.nitdoc_url}", "{mmodule.nitdoc_name}"), false)
-               topmenu.add_item(new TplLink("#", "{mclass.nitdoc_name}"), true)
-               topmenu.add_item(new TplLink("search.html", "Index"), false)
+               var mproject = mclass.intro_mmodule.mgroup.mproject
+               topmenu.add_link new TplLink("{mproject.nitdoc_url}", "{mproject.nitdoc_name}")
+               topmenu.add_link new TplLink(page_url, mclass.nitdoc_name)
                return topmenu
        end
 
        # Property list to display in sidebar
        fun tpl_sidebar_properties do
-               var kind_map = sort_by_kind(mclass_inherited_mprops)
+               var by_kind = new PropertiesByKind.with_elements(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)
+               by_kind.sort_groups(name_sorter)
+               for g in by_kind.groups do tpl_sidebar_list(g, summary)
                tpl_sidebar.boxes.add new TplSideBox.with_content("All properties", summary)
        end
 
-       private fun tpl_sidebar_list(name: String, mprops: Array[MProperty], summary: TplList) do
+       private fun tpl_sidebar_list(mprops: PropertyGroup[MProperty], summary: TplList) do
                if mprops.is_empty then return
-               name_sorter.sort(mprops)
-               var entry = new TplListItem.with_content(name)
+               var entry = new TplListItem.with_content(mprops.title)
                var list = new TplList.with_classes(["list-unstyled", "list-labeled"])
                for mprop in mprops do
                        list.add_li tpl_sidebar_item(mprop)
@@ -817,14 +1120,19 @@ class NitdocClass
                summary.elts.add entry
        end
 
-       private fun tpl_sidebar_item(mprop: MProperty): Template do
+       private fun tpl_sidebar_item(mprop: MProperty): TplListItem 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
+                       var cls_url = mprop.intro.mclassdef.mclass.nitdoc_url
+                       var def_url = "{cls_url}#{mprop.nitdoc_id}"
+                       var lnk = new TplLink(def_url, mprop.name)
+                       var mdoc = mprop.intro.mdoc_or_fallback
+                       if mdoc != null then lnk.title = mdoc.short_comment
+                       var item = new Template
+                       item.add new TplLabel.with_classes(classes)
+                       item.add lnk
+                       return new TplListItem.with_content(item)
                end
                var defs = mprops2mdefs[mprop]
                if defs.has(mprop.intro) then
@@ -834,91 +1142,60 @@ class NitdocClass
                end
                var lnk = new Template
                lnk.add new TplLabel.with_classes(classes)
-               lnk.add mprop.intro.tpl_anchor
-               return lnk
+               lnk.add mprop.tpl_anchor
+               return new TplListItem.with_content(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
+               var section = new TplSection.with_title("top", tpl_title)
+               section.subtitle = mclass.intro.tpl_declaration
+               var article = new TplArticle("comment")
+               var mdoc = mclass.mdoc_or_fallback
+               if mdoc != null then
+                       article.content = mdoc.tpl_comment
+               end
                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")
-                       name_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"])
-                                       name_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
+       private fun tpl_concerns(parent: TplSection) do
+               # intro title
+               var section = new TplSection.with_title("intro", "Introduction")
+               section.summary_title = "Introduction"
+               section.add_child tpl_mclassdef_article(mclass.intro)
+               parent.add_child section
+               # concerns
+               if concerns.is_empty then return
+               parent.add_child new TplArticle.with_content("concerns", "Concerns", concerns.to_tpl)
        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
+               for c in mclass.in_hierarchy(mainmodule).direct_greaters do
+                       if ctx.filter_mclass(c) then hparents.add c
                end
 
                # ancestors
                var hancestors = new HashSet[MClass]
-               for c in mclass.in_hierarchy(ctx.mainmodule).greaters do
+               for c in mclass.in_hierarchy(mainmodule).greaters do
                        if c == mclass then continue
-                       if c.visibility < ctx.min_visibility then continue
+                       if not ctx.filter_mclass(c) 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
+               for c in mclass.in_hierarchy(mainmodule).direct_smallers do
+                       if ctx.filter_mclass(c) then hchildren.add c
                end
 
                # descendants
                var hdescendants = new HashSet[MClass]
-               for c in mclass.in_hierarchy(ctx.mainmodule).smallers do
+               for c in mclass.in_hierarchy(mainmodule).smallers do
                        if c == mclass then continue
-                       if c.visibility < ctx.min_visibility then continue
+                       if not ctx.filter_mclass(c) then continue
                        if hchildren.has(c) then continue
                        hdescendants.add c
                end
@@ -930,8 +1207,8 @@ class NitdocClass
                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_all hchildren
+               mclasses.add_all hdescendants
                mclasses.add mclass
                var graph = tpl_dot(mclasses)
                if graph != null then section.add_child graph
@@ -951,14 +1228,14 @@ class NitdocClass
                end
 
                # children
-               if not hchildren.is_empty and hchildren.length < 15 then
+               if not hchildren.is_empty then
                        var lst = hchildren.to_a
                        name_sorter.sort lst
                        section.add_child tpl_list("children", "Children", lst)
                end
 
                # descendants
-               if not hdescendants.is_empty and hchildren.length < 15 then
+               if not hdescendants.is_empty then
                        var lst = hdescendants.to_a
                        name_sorter.sort lst
                        section.add_child tpl_list("descendants", "Descendants", lst)
@@ -969,62 +1246,79 @@ class NitdocClass
 
        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
+               if elts.length > 20 then
+                       var tpl = new Template
+                       for e in elts do
+                               tpl.add e.tpl_link
+                               if e != elts.last then tpl.add ", "
+                       end
+                       article.content = tpl
+               else
+                       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
+               end
                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
+               var lst = concerns.to_a
+               for mentity in lst do
+                       if mentity isa MProject then
+                               parent.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MGroup then
+                               parent.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MModule then
+                               var section = new TplSection(mentity.nitdoc_id)
+                               var title = new Template
+                               title.add "in "
+                               title.add mentity.tpl_namespace
+                               section.title = title
+                               section.summary_title = "in {mentity.nitdoc_name}"
+
                                # properties
-                               var mprops = mod_map[mmodule]
-                               var kind_map = sort_by_kind(mprops)
-
-                               # virtual types
-                               var elts = kind_map["type"].to_a
-                               name_sorter.sort(elts)
-                               for elt in elts do
-                                       var defs = mprops2mdefs[elt].to_a
-                                       section.add_child tpl_mprop_article(elt, defs)
-                               end
+                               var mprops = mmodules2mprops[mentity]
+                               var by_kind = new PropertiesByKind.with_elements(mprops)
 
-                               # constructors
-                               elts = kind_map["init"].to_a
-                               name_sorter.sort(elts)
-                               for elt in elts do
-                                       var defs = mprops2mdefs[elt].to_a
-                                       section.add_child tpl_mprop_article(elt, defs)
+                               for g in by_kind.groups do
+                                       for article in tpl_mproperty_articles(g) do
+                                               section.add_child article
+                                       end
                                end
+                               parent.add_child section
+                       end
+               end
+       end
 
-                               # methods
-                               elts = kind_map["fun"].to_a
-                               name_sorter.sort(elts)
-                               for elt in elts do
-                                       var defs = mprops2mdefs[elt].to_a
-                                       section.add_child tpl_mprop_article(elt, defs)
+       private fun tpl_mproperty_articles(elts: Collection[MProperty]):
+                       Sequence[TplArticle] do
+               var articles = new List[TplArticle]
+               for elt in elts do
+                       var local_defs = mprops2mdefs[elt]
+                       # var all_defs = elt.mpropdefs
+                       var all_defs = new HashSet[MPropDef]
+                       for local_def in local_defs do
+                               all_defs.add local_def
+                               var mpropdef = local_def
+                               while not mpropdef.is_intro do
+                                       mpropdef = mpropdef.lookup_next_definition(mainmodule, mpropdef.mclassdef.bound_mtype)
+                                       all_defs.add mpropdef
                                end
                        end
-                       parent.add_child section
+                       var loc_lin = local_defs.to_a
+                       mainmodule.linearize_mpropdefs(loc_lin)
+                       var all_lin = all_defs.to_a
+                       mainmodule.linearize_mpropdefs(all_lin)
+                       articles.add tpl_mprop_article(loc_lin.first, loc_lin, all_lin)
                end
+               return articles
        end
 
        redef fun tpl_content do
                tpl_sidebar_properties
                var top = tpl_intro
-               tpl_concerns(top)
                tpl_inheritance(top)
+               tpl_concerns(top)
                tpl_properties(top)
                tpl_page.add_section top
        end
@@ -1043,7 +1337,7 @@ class NitdocClass
                var map = new HashMap[MModule, Set[MProperty]]
                for mprop in mprops do
                        var mpropdefs = mprops2mdefs[mprop].to_a
-                       ctx.mainmodule.linearize_mpropdefs(mpropdefs)
+                       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
@@ -1051,29 +1345,10 @@ class NitdocClass
                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
+               for mprop in mclass.inherited_mproperties(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
@@ -1095,17 +1370,6 @@ class NitdocClass
                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]
@@ -1113,28 +1377,226 @@ class NitdocClass
                for mclass in mclasses do
                        poset.add_node mclass
                        for oclass in mclasses do
+                               if mclass == oclass then continue
                                poset.add_node oclass
-                               if mclass.in_hierarchy(ctx.mainmodule) < oclass then
+                               if mclass.in_hierarchy(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
+               var op = new RopeBuffer
+               var name = "dep_class_{mclass.nitdoc_id}"
+               op.append("digraph \"{name.escape_to_dot}\" \{ 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")
+               var classes = poset.to_a
+               var todo = new Array[MClass]
+               var done = new HashSet[MClass]
+               mainmodule.linearize_mclasses(classes)
+               if not classes.is_empty then todo.add classes.first
+               while not todo.is_empty do
+                       var c = todo.shift
+                       if done.has(c) then continue
+                       done.add c
                        if c == mclass then
-                               op.append("\"{c.name}\"[shape=box,margin=0.03];\n")
+                               op.append("\"{c.name.escape_to_dot}\"[shape=box,margin=0.03];\n")
                        else
-                               op.append("\"{c.name}\"[URL=\"{c.nitdoc_url}\"];\n")
+                               op.append("\"{c.name.escape_to_dot}\"[URL=\"{c.nitdoc_url.escape_to_dot}\"];\n")
                        end
-                       for c2 in poset[c].direct_greaters do
-                               op.append("\"{c.name}\"->\"{c2.name}\";\n")
+                       var smallers = poset[c].direct_smallers
+                       if smallers.length < 10 then
+                               for c2 in smallers do
+                                       op.append("\"{c2.name.escape_to_dot}\"->\"{c.name.escape_to_dot}\";\n")
+                               end
+                               todo.add_all smallers
+                       else
+                               op.append("\"...\"->\"{c.name.escape_to_dot}\";\n")
                        end
                end
                op.append("\}\n")
-               return tpl_graph(op, name, "Inheritance graph")
+               return tpl_graph(op, name, null)
+       end
+end
+
+# Groups properties by kind.
+private class PropertiesByKind
+       # The virtual types.
+       var virtual_types = new PropertyGroup[MVirtualTypeProp]("Virtual types")
+
+       # The constructors.
+       var constructors = new PropertyGroup[MMethod]("Contructors")
+
+       # The attributes.
+       var attributes = new PropertyGroup[MAttribute]("Attributes")
+
+       # The methods.
+       var methods = new PropertyGroup[MMethod]("Methods")
+
+       # The inner classes.
+       var inner_classes = new PropertyGroup[MInnerClass]("Inner classes")
+
+       # All the groups.
+       #
+       # Sorted in the order they are displayed to the user.
+       var groups: SequenceRead[PropertyGroup[MProperty]] = [
+                       virtual_types,
+                       constructors,
+                       attributes,
+                       methods,
+                       inner_classes: PropertyGroup[MProperty]]
+
+       # Add each the specified property to the appropriate list.
+       init with_elements(properties: Collection[MProperty]) do add_all(properties)
+
+       # Add the specified property to the appropriate list.
+       fun add(property: MProperty) do
+               if property isa MMethod then
+                       if property.is_init then
+                               constructors.add property
+                       else
+                               methods.add property
+                       end
+               else if property isa MVirtualTypeProp then
+                       virtual_types.add property
+               else if property isa MAttribute then
+                       attributes.add property
+               else if property isa MInnerClass then
+                       inner_classes.add property
+               else
+                       abort
+               end
+       end
+
+       # Add each the specified property to the appropriate list.
+       fun add_all(properties: Collection[MProperty]) do
+               for p in properties do add(p)
+       end
+
+       # Sort each group with the specified comparator.
+       fun sort_groups(comparator: Comparator) do
+               for g in groups do comparator.sort(g)
+       end
+end
+
+# A Group of properties of the same kind.
+private class PropertyGroup[E: MProperty]
+       super Array[E]
+
+       # The title of the group, as displayed to the user.
+       var title: String
+end
+
+# A MProperty page
+class NitdocProperty
+       super NitdocPage
+
+       private var mproperty: MProperty
+       private var concerns: ConcernsTree is noinit
+       private var mmodules2mdefs: Map[MModule, Set[MPropDef]] is noinit
+
+       init do
+               self.mproperty = mproperty
+               self.mmodules2mdefs = sort_by_mmodule(collect_mpropdefs)
+               self.concerns = model.concerns_tree(mmodules2mdefs.keys)
+               self.concerns.sort_with(new MConcernRankSorter)
+       end
+
+       private fun collect_mpropdefs: Set[MPropDef] do
+               var res = new HashSet[MPropDef]
+               for mpropdef in mproperty.mpropdefs do
+                       if not mpropdef.is_intro then res.add mpropdef
+               end
+               return res
+       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 "{mproperty.nitdoc_name}{mproperty.tpl_signature.write_to_string}"
+       end
+
+       redef fun page_url do return mproperty.nitdoc_url
+
+       redef fun tpl_topmenu do
+               var topmenu = super
+               var mmodule = mproperty.intro_mclassdef.mmodule
+               var mproject = mmodule.mgroup.mproject
+               var mclass = mproperty.intro_mclassdef.mclass
+               topmenu.add_link new TplLink("{mproject.nitdoc_url}", "{mproject.nitdoc_name}")
+               topmenu.add_link new TplLink("{mclass.nitdoc_url}", "{mclass.nitdoc_name}")
+               topmenu.add_link new TplLink(page_url, mproperty.nitdoc_name)
+               return topmenu
+       end
+
+       private fun tpl_intro: TplSection do
+               var title = new Template
+               title.add mproperty.nitdoc_name
+               title.add mproperty.intro.tpl_signature
+               var section = new TplSection.with_title("top", title)
+               section.subtitle = mproperty.tpl_namespace
+               section.summary_title = mproperty.nitdoc_name
+               return section
+       end
+
+       private fun tpl_properties(parent: TplSection) do
+               # intro title
+               var ns = mproperty.intro.mclassdef.mmodule.tpl_namespace
+               var section = new TplSection("intro")
+               var title = new Template
+               title.add "Introduction in "
+               title.add ns
+               section.title = title
+               section.summary_title = "Introduction"
+               section.add_child tpl_mpropdef_article(mproperty.intro)
+               parent.add_child section
+
+               # concerns
+               if concerns.is_empty then return
+               parent.add_child new TplArticle.with_content("Concerns", "Concerns", concerns.to_tpl)
+
+               # redef list
+               var lst = concerns.to_a
+               for mentity in lst do
+                       if mentity isa MProject then
+                               parent.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MGroup then
+                               parent.add_child new TplSection(mentity.nitdoc_id)
+                       else if mentity isa MModule then
+                               var ssection = new TplSection(mentity.nitdoc_id)
+                               title = new Template
+                               title.add "in "
+                               title.add mentity.tpl_namespace
+                               ssection.title = title
+                               ssection.summary_title = "in {mentity.nitdoc_name}"
+
+                               # properties
+                               var mpropdefs = mmodules2mdefs[mentity].to_a
+                               name_sorter.sort(mpropdefs)
+                               for mpropdef in mpropdefs do
+                                       ssection.add_child tpl_mpropdef_article(mpropdef)
+                               end
+                               parent.add_child ssection
+                       end
+               end
+       end
+
+       redef fun tpl_content do
+               var top = tpl_intro
+               tpl_properties(top)
+               tpl_page.add_section top
+       end
+
+       private fun sort_by_mmodule(mpropdefs: Collection[MPropDef]): Map[MModule, Set[MPropDef]] do
+               var map = new HashMap[MModule, Set[MPropDef]]
+               for mpropdef in mpropdefs do
+                       var mmodule = mpropdef.mclassdef.mmodule
+                       if not map.has_key(mmodule) then map[mmodule] = new HashSet[MPropDef]
+                       map[mmodule].add mpropdef
+               end
+               return map
        end
 end