nitdoc: introduce MakePagePhase
[nit.git] / src / doc / doc_pages.nit
index 4ebd217..604b415 100644 (file)
@@ -17,6 +17,7 @@ module doc_pages
 
 import toolcontext
 import doc_model
+private import json::static
 
 redef class ToolContext
        private var opt_dir = new OptionString("output directory", "-d", "--dir")
@@ -24,7 +25,6 @@ redef class ToolContext
        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_brand = new OptionString("custom link to external site", "--custom-brand")
@@ -45,15 +45,11 @@ redef class ToolContext
                super
 
                var opts = option_context
-               opts.add_option(opt_dir, opt_source, opt_sharedir, opt_shareurl, opt_nodot, opt_private)
+               opts.add_option(opt_dir, opt_source, opt_sharedir, opt_shareurl,
+                               opt_nodot)
                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)
-
-               var tpl = new Template
-               tpl.add "Usage: nitdoc [OPTION]... <file.nit>...\n"
-               tpl.add "Generates HTML pages of API documentation from Nit source files."
-               tooldescription = tpl.write_to_string
        end
 
        redef fun process_options(args) do
@@ -65,12 +61,6 @@ redef class ToolContext
                        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
@@ -90,17 +80,6 @@ class Nitdoc
        var model: Model
        var mainmodule: MModule
 
-       fun generate do
-               init_output_dir
-               overview
-               search
-               groups
-               modules
-               classes
-               properties
-               quicksearch_list
-       end
-
        private fun init_output_dir do
                # create destination dir if it's necessary
                var output_dir = ctx.output_dir
@@ -117,60 +96,12 @@ class Nitdoc
                end
                # copy shared files
                if ctx.opt_shareurl.value == null then
-                       sys.system("cp -r {sharedir.to_s}/* {output_dir.to_s}/")
+                       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 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 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 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 model.mclasses do
-                       if mclass.visibility <= ctx.min_visibility 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 mproperty.visibility <= ctx.min_visibility 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(ctx, model)
-               quicksearch.render.write_to_file("{ctx.output_dir.to_s}/quicksearch-list.js")
-       end
 end
 
 # Nitdoc QuickSearch list generator
@@ -182,61 +113,79 @@ 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 do
                for mmodule in model.mmodules do
-                       if mmodule.is_fictive then continue
-                       mmodules.add mmodule
+                       if mmodule.is_fictive or mmodule.is_test_suite then continue
+                       add_result_for(mmodule.name, mmodule.full_name, mmodule.nitdoc_url)
                end
                for mclass in model.mclasses do
-                       if mclass.visibility < ctx.min_visibility then continue
-                       mclasses.add mclass
+                       if not ctx.filter_mclass(mclass) then continue
+                       add_result_for(mclass.name, mclass.full_name, mclass.nitdoc_url)
                end
                for mproperty in 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]
+                       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
-                               var cls_url = mpropdef.mclassdef.mclass.nitdoc_url
-                               var def_url = "{cls_url}#{mpropdef.mproperty.nitdoc_id}"
-                               tpl.add "\{txt:\"{full_name}\",url:\"{def_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
@@ -314,11 +263,13 @@ abstract class NitdocPage
        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
 
@@ -326,11 +277,12 @@ abstract class NitdocPage
                var alt = ""
                if title != null then
                        article.title = title
-                       alt = "alt='{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}/>"
+               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
@@ -343,7 +295,7 @@ abstract class NitdocPage
                var source = ctx.opt_source.value
                if source == null then
                        var url = location.file.filename.simplify_path
-                       return "<a target='_blank' title='Show source' href=\"{url}\">View Source</a>"
+                       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")
@@ -353,7 +305,7 @@ 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}\">View Source</a>"
+               return "<a target='_blank' title='Show source' href=\"{source.to_s.html_escape}\">View Source</a>"
        end
 
        # MProject description template
@@ -361,8 +313,9 @@ abstract class NitdocPage
                var article = mproject.tpl_article
                article.subtitle = mproject.tpl_declaration
                article.content = mproject.tpl_definition
-               if mproject.mdoc != null then
-                       article.content = mproject.mdoc.tpl_short_comment
+               var mdoc = mproject.mdoc_or_fallback
+               if mdoc != null then
+                       article.content = mdoc.tpl_short_comment
                end
                return article
        end
@@ -384,7 +337,7 @@ abstract class NitdocPage
                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_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
@@ -397,7 +350,7 @@ abstract class NitdocPage
                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_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
@@ -426,7 +379,7 @@ abstract class NitdocPage
                        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 = 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
@@ -435,7 +388,7 @@ abstract class NitdocPage
                                intros.content = intros_lst
                                redef_article.add_child intros
                        end
-                       var redefs = new TplArticle.with_title("{mclassdef.nitdoc_id}_redefs", "Redefines")
+                       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
@@ -474,7 +427,8 @@ abstract class NitdocPage
                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")
+                       var lnk = new TplLink.with_title(def_url, mprop.nitdoc_name,
+                                       "Go to introduction")
                        title.add "redef "
                        title.add lnk
                end
@@ -482,10 +436,10 @@ abstract class NitdocPage
                article.title_classes.add "signature"
                article.summary_title = "{mprop.nitdoc_name}"
                article.subtitle = main_mpropdef.tpl_namespace
-               if main_mpropdef.mdoc != null then
-                       article.content = main_mpropdef.mdoc.tpl_comment
+               if main_mpropdef.mdoc_or_fallback != null then
+                       article.content = main_mpropdef.mdoc_or_fallback.tpl_comment
                end
-               var subarticle = new TplArticle("{main_mpropdef.nitdoc_id}_redefs")
+               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
@@ -507,7 +461,7 @@ abstract class NitdocPage
                end
                # Add linearization
                if lin.length > 1 then
-                       var lin_article = new TplArticle("{main_mpropdef.nitdoc_id}_lin")
+                       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
@@ -622,7 +576,7 @@ class NitdocSearch
        private fun modules_list: Array[MModule] do
                var sorted = new Array[MModule]
                for mmodule in model.mmodule_importation_hierarchy do
-                       if mmodule.is_fictive then continue
+                       if mmodule.is_fictive or mmodule.is_test_suite then continue
                        sorted.add mmodule
                end
                name_sorter.sort(sorted)
@@ -633,7 +587,7 @@ class NitdocSearch
        private fun classes_list: Array[MClass] do
                var sorted = new Array[MClass]
                for mclass in model.mclasses do
-                       if mclass.visibility < ctx.min_visibility then continue
+                       if not ctx.filter_mclass(mclass) then continue
                        sorted.add mclass
                end
                name_sorter.sort(sorted)
@@ -644,9 +598,7 @@ class NitdocSearch
        private fun mprops_list: Array[MProperty] do
                var sorted = new Array[MProperty]
                for mproperty in model.mproperties do
-                       if mproperty.visibility < ctx.min_visibility then continue
-                       if mproperty isa MAttribute then continue
-                       sorted.add mproperty
+                       if ctx.filter_mproperty(mproperty) then sorted.add mproperty
                end
                name_sorter.sort(sorted)
                return sorted
@@ -880,7 +832,7 @@ class NitdocModule
 
                # Graph
                var mmodules = new HashSet[MModule]
-               mmodules.add_all mmodule.in_nesting.direct_greaters
+               mmodules.add_all mmodule.nested_mmodules
                mmodules.add_all imports
                if clients.length < 10 then mmodules.add_all clients
                mmodules.add mmodule
@@ -890,7 +842,7 @@ class NitdocModule
                # Imports
                var lst = new Array[MModule]
                for dep in imports do
-                       if dep.is_fictive then continue
+                       if dep.is_fictive or dep.is_test_suite then continue
                        if dep == mmodule then continue
                        lst.add(dep)
                end
@@ -902,7 +854,7 @@ class NitdocModule
                # Clients
                lst = new Array[MModule]
                for dep in clients do
-                       if dep.is_fictive then continue
+                       if dep.is_fictive or dep.is_test_suite then continue
                        if dep == mmodule then continue
                        lst.add(dep)
                end
@@ -981,10 +933,10 @@ class NitdocModule
        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
+                       if mmodule.is_fictive or mmodule.is_test_suite then continue
                        poset.add_node mmodule
                        for omodule in mmodules do
-                               if mmodule.is_fictive then continue
+                               if omodule.is_fictive or omodule.is_test_suite then continue
                                poset.add_node mmodule
                                if mmodule.in_importation < omodule then
                                        poset.add_edge(mmodule, omodule)
@@ -994,15 +946,15 @@ class NitdocModule
                # build graph
                var op = new RopeBuffer
                var name = "dep_module_{mmodule.nitdoc_id}"
-               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")
+               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")
@@ -1059,19 +1011,17 @@ class NitdocClass
 
        # 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)
@@ -1086,8 +1036,9 @@ class NitdocClass
                        classes.add "inherit"
                        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)
-                       if mprop.intro.mdoc != null then lnk.title = mprop.intro.mdoc.short_comment
+                       var lnk = new TplLink(def_url, mprop.nitdoc_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
@@ -1109,8 +1060,9 @@ class NitdocClass
                var section = new TplSection.with_title("top", tpl_title)
                section.subtitle = mclass.intro.tpl_declaration
                var article = new TplArticle("comment")
-               if mclass.mdoc != null then
-                       article.content = mclass.mdoc.tpl_comment
+               var mdoc = mclass.mdoc_or_fallback
+               if mdoc != null then
+                       article.content = mdoc.tpl_comment
                end
                section.add_child article
                return section
@@ -1131,15 +1083,14 @@ class NitdocClass
                # parents
                var hparents = new HashSet[MClass]
                for c in mclass.in_hierarchy(mainmodule).direct_greaters do
-                       if c.visibility < ctx.min_visibility then continue
-                       hparents.add c
+                       if ctx.filter_mclass(c) then hparents.add c
                end
 
                # ancestors
                var hancestors = new HashSet[MClass]
                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
@@ -1147,15 +1098,14 @@ class NitdocClass
                # children
                var hchildren = new HashSet[MClass]
                for c in mclass.in_hierarchy(mainmodule).direct_smallers do
-                       if c.visibility < ctx.min_visibility then continue
-                       hchildren.add c
+                       if ctx.filter_mclass(c) then hchildren.add c
                end
 
                # descendants
                var hdescendants = new HashSet[MClass]
                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
@@ -1238,30 +1188,21 @@ class NitdocClass
 
                                # properties
                                var mprops = mmodules2mprops[mentity]
-                               var kind_map = sort_by_kind(mprops)
+                               var by_kind = new PropertiesByKind.with_elements(mprops)
 
-                               # virtual types
-                               for article in tpl_mproperty_articles(kind_map, "type") do
-                                       section.add_child article
-                               end
-                               # constructors
-                               for article in tpl_mproperty_articles(kind_map, "init") do
-                                       section.add_child article
-                               end
-                               # methods
-                               for article in tpl_mproperty_articles(kind_map, "fun") do
-                                       section.add_child article
+                               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
 
-       private fun tpl_mproperty_articles(kind_map: Map[String, Set[MProperty]],
-               kind_name: String): Sequence[TplArticle] do
+       private fun tpl_mproperty_articles(elts: Collection[MProperty]):
+                       Sequence[TplArticle] do
                var articles = new List[TplArticle]
-               var elts = kind_map[kind_name].to_a
-               name_sorter.sort(elts)
                for elt in elts do
                        var local_defs = mprops2mdefs[elt]
                        # var all_defs = elt.mpropdefs
@@ -1314,25 +1255,6 @@ 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)
@@ -1375,7 +1297,7 @@ class NitdocClass
 
                var op = new RopeBuffer
                var name = "dep_class_{mclass.nitdoc_id}"
-               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")
+               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]
@@ -1386,18 +1308,18 @@ class NitdocClass
                        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
                        var smallers = poset[c].direct_smallers
                        if smallers.length < 10 then
                                for c2 in smallers do
-                                       op.append("\"{c2.name}\"->\"{c.name}\";\n")
+                                       op.append("\"{c2.name.escape_to_dot}\"->\"{c.name.escape_to_dot}\";\n")
                                end
                                todo.add_all smallers
                        else
-                               op.append("\"...\"->\"{c.name}\";\n")
+                               op.append("\"...\"->\"{c.name.escape_to_dot}\";\n")
                        end
                end
                op.append("\}\n")
@@ -1463,7 +1385,12 @@ class NitdocProperty
 
        private fun tpl_properties(parent: TplSection) do
                # intro title
-               var section = new TplSection.with_title("intro", "Introduction")
+               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
@@ -1481,7 +1408,7 @@ class NitdocProperty
                                parent.add_child new TplSection(mentity.nitdoc_id)
                        else if mentity isa MModule then
                                var ssection = new TplSection(mentity.nitdoc_id)
-                               var title = new Template
+                               title = new Template
                                title.add "in "
                                title.add mentity.tpl_namespace
                                ssection.title = title