Merge: Generalize instance creation service so FFI can use it
authorJean Privat <jean@pryen.org>
Fri, 6 Feb 2015 01:14:55 +0000 (08:14 +0700)
committerJean Privat <jean@pryen.org>
Fri, 6 Feb 2015 01:14:55 +0000 (08:14 +0700)
Move up instance creation from ANew to AbstractCompilerVisitor so they can be used from the FFI implementation. Allows to call extern constructors of extern classes from extern code, and fix #1145.

Pull-Request: #1150
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>
Reviewed-by: Jean Privat <jean@pryen.org>

36 files changed:
contrib/github_merge.nit
contrib/nitrpg/.gitignore [new file with mode: 0644]
contrib/nitrpg/Makefile [new file with mode: 0644]
contrib/nitrpg/src/game.nit [new file with mode: 0644]
contrib/nitrpg/src/listener.nit [new file with mode: 0644]
contrib/nitrpg/src/reactors.nit [new file with mode: 0644]
contrib/nitrpg/src/statistics.nit [new file with mode: 0644]
contrib/nitrpg/src/templates/panels.nit [new file with mode: 0644]
contrib/nitrpg/src/templates/templates.nit [new file with mode: 0644]
contrib/nitrpg/src/templates/templates_base.nit [new file with mode: 0644]
contrib/nitrpg/src/web.nit [new file with mode: 0644]
contrib/nitrpg/www/styles/main.css [new file with mode: 0644]
lib/standard/string.nit
lib/standard/string_nit.c
lib/standard/string_nit.h
lib/standard/time.nit
src/Makefile
src/compiler/abstract_compiler.nit
src/doc/doc.nit
src/doc/doc_base.nit [new file with mode: 0644]
src/doc/doc_model.nit
src/doc/doc_pages.nit [deleted file]
src/doc/doc_phases/doc_concerns.nit [new file with mode: 0644]
src/doc/doc_phases/doc_extract.nit [new file with mode: 0644]
src/doc/doc_phases/doc_graphs.nit [new file with mode: 0644]
src/doc/doc_phases/doc_hierarchies.nit [new file with mode: 0644]
src/doc/doc_phases/doc_html.nit [new file with mode: 0644]
src/doc/doc_phases/doc_indexing.nit [new file with mode: 0644]
src/doc/doc_phases/doc_intros_redefs.nit [new file with mode: 0644]
src/doc/doc_phases/doc_pages.nit [new file with mode: 0644]
src/doc/doc_phases/doc_phases.nit [new file with mode: 0644]
src/doc/doc_phases/doc_poset.nit [new file with mode: 0644]
src/doc/doc_phases/doc_structure.nit [new file with mode: 0644]
src/interpreter/naive_interpreter.nit
src/loader.nit
src/nitdoc.nit

index c85f4af..8a2c545 100644 (file)
@@ -35,7 +35,12 @@ redef class GithubCurl
                var statuses = get_and_check("https://api.github.com/repos/privat/nit/statuses/{sha}")
                prm["statuses"] = statuses
                print "{prm["title"].to_s}: by {prm["user"].json_as_map["login"].to_s} (# {prm["number"].to_s})"
-               print "\tmergable: {prm["mergeable"].to_s}"
+               var mergeable = prm["mergeable"]
+               if mergeable != null then
+                       print "\tmergeable: {mergeable.to_s}"
+               else
+                       print "\tmergeable: unknown"
+               end
                var st = prm["statuses"].json_as_a
                if not st.is_empty then
                        print "\tstatus: {st[0].json_as_map["state"].to_s}"
@@ -115,12 +120,13 @@ else
                print "Commit {sha} not in local repository; did you fetch github?"
                return
        end
-       if system("git merge --no-commit {sha}") != 0 then
+       if system("git merge --no-ff --no-commit {sha}") != 0 then
                system("cp mergemsg `git rev-parse --git-dir`/MERGE_MSG")
                print "Problem during merge... Let's do the commit manually."
                return
        end
        system("git commit -F mergemsg")
        print "The merge is made"
+       mergemsg.write_to(stdout)
 end
 
diff --git a/contrib/nitrpg/.gitignore b/contrib/nitrpg/.gitignore
new file mode 100644 (file)
index 0000000..24c4036
--- /dev/null
@@ -0,0 +1,4 @@
+.github_data
+nitrpg_data
+listener
+web
diff --git a/contrib/nitrpg/Makefile b/contrib/nitrpg/Makefile
new file mode 100644 (file)
index 0000000..40ebec2
--- /dev/null
@@ -0,0 +1,28 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+NITC=../../bin/nitc
+
+all: listener web
+
+listener:
+       $(NITC) src/listener.nit
+
+web:
+       $(NITC) src/web.nit
+
+clean:
+       rm listener web
diff --git a/contrib/nitrpg/src/game.nit b/contrib/nitrpg/src/game.nit
new file mode 100644 (file)
index 0000000..9de4887
--- /dev/null
@@ -0,0 +1,294 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# `nitrpg` game structures.
+#
+# Here we define the main game entities:
+#
+# * `Game` holds all the entities for a game and provides high level services.
+# * `Player` represents a `Github::User` which plays the `Game`.
+#
+# Developpers who wants to extend the game capabilities should look at
+# the `GameReactor` abstraction.
+module game
+
+intrude import json::store
+import github::events
+
+# An entity within a `Game`.
+#
+# All game entities can be saved in a json format.
+interface GameEntity
+       # The game instance containing `self`.
+       fun game: Game is abstract
+
+       # Uniq key used for data storage.
+       fun key: String is abstract
+
+       # Save `self` as a json object.
+       fun save do game.store.store_object(key, to_json)
+
+       # Json representation of `self`.
+       fun to_json: JsonObject  do return new JsonObject
+
+       # Pretty print `self` to be displayed in a terminal.
+       fun pretty: String is abstract
+end
+
+# Holder for game data and main services.
+#
+# Game is a `GameEntity` so it can be saved.
+class Game
+       super GameEntity
+
+       redef fun game do return self
+
+       # Returns the repo `full_name`.
+       #
+       # Example: `"privat/nit"`
+       redef fun key do return repo.full_name
+
+       # We need a `GithubAPI` client to load Github data.
+       var api: GithubAPI
+
+       # A game takes place in a `github::Repo`.
+       var repo: Repo
+
+       # Directory where game data are stored.
+       var game_dir: String is lazy do return "nitrpg_data" / repo.full_name
+
+       # Used for data storage.
+       #
+       # File are stored in `game_dir`.
+       var store: JsonStore is lazy do return new JsonStore(game_dir)
+
+       # Init the Game and try to load saved data.
+       init do if store.has_key(key) then from_json(store.load_object(key))
+
+       # Init `self` from a JsonObject.
+       #
+       # Used to load entities from saved data.
+       fun from_json(json: JsonObject) do end
+
+       # Create a player from a Github `User`.
+       #
+       # Or return the existing one from game data.
+       fun add_player(user: User): Player do
+               # check if player already exists
+               var player = load_player(user.login)
+               if player != null then return player
+               # create and store new player
+               player = new Player(self, user.login)
+               player.save
+               return player
+       end
+
+       # Get a Player from his `name` or null if no player was found.
+       #
+       # Looks for the player save file in game data.
+       #
+       # Returns `null` if the player cannot be found.
+       # In this case, the player can be created with `add_player`.
+       fun load_player(name: String): nullable Player do
+               var key = "players" / name
+               if not store.has_key(key) then return null
+               var json = store.load_object(key)
+               return new Player.from_json(self, json)
+       end
+
+       # List known players.
+       #
+       # This list is reloaded from game data each time its called.
+       #
+       # To add players see `add_player`.
+       fun load_players: MapRead[String, Player] do
+               var res = new HashMap[String, Player]
+               if not store.has_collection("players") then return res
+               var coll = store.list_collection("players")
+               for id in coll do
+                       var name = id.to_s
+                       res[name] = load_player(name).as(not null)
+               end
+               return res
+       end
+
+       # Return a list of player name associated to their rank in the game.
+       fun player_ranking: MapRead[String, Int] do
+               var arr = load_players.values.to_a
+               var res = new HashMap[String, Int]
+               (new PlayerCoinComparator).sort(arr)
+               var rank = 1
+               for player in arr do
+                       res[player.name] = rank
+                       rank += 1
+               end
+               return res
+       end
+
+       # Erase all saved data for this game.
+       fun clear do store.clear
+
+       # Verbosity level used fo stdout.
+       #
+       # * `-1` quiet
+       # * `0` error and warnings
+       # * `1` info
+       # * `2` debug
+       var verbose_lvl = 0 is writable
+
+       # Display `msg` if `lvl` >= `verbose_lvl`
+       fun message(lvl: Int, msg: String) do
+               if lvl > verbose_lvl then return
+               print msg
+       end
+
+       redef fun pretty do
+               var res = new FlatBuffer
+               res.append "-------------------------\n"
+               res.append "{repo.full_name}\n"
+               res.append "-------------------------\n"
+               res.append "# {load_players.length} players \n"
+               return res.write_to_string
+       end
+end
+
+# Players can battle on nitrpg for nitcoins and glory.
+#
+# A `Player` is linked to a `Github::User`.
+class Player
+       super GameEntity
+
+       # Key is based on player `name`.
+       redef var key is lazy do return "players" / name
+
+       redef var game
+
+       # FIXME contructor should be private
+
+       # Player name.
+       #
+       # This is the unic key for this player.
+       # Should be equal to the associated `Github::User::login`.
+       #
+       # The name is also used to load the user data lazilly from Github API.
+       var name: String
+
+       # Player amount of nitcoins.
+       #
+       # Nitcoins is the currency used in nitrpg.
+       # They can be obtained by performing actions on the `Game::Repo`.
+       var nitcoins: Int = 0 is public writable
+
+       # `Github::User` linked to this player.
+       var user: User is lazy do
+               var user = game.api.load_user(name)
+               assert user isa User
+               return user
+       end
+
+       # Init `self` from a `json` object.
+       #
+       # Used to load players from saved data.
+       init from_json(game: Game, json: JsonObject) do
+               self.game = game
+               name = json["name"].to_s
+               nitcoins = json["nitcoins"].as(Int)
+       end
+
+       redef fun to_json do
+               var json = super
+               json["name"] = name
+               json["nitcoins"] = nitcoins
+               return json
+       end
+
+       redef fun pretty do
+               var res = new FlatBuffer
+               res.append "-- {name} ({nitcoins} $)\n"
+               return res.write_to_string
+       end
+
+       redef fun to_s do return name
+end
+
+redef class User
+       # The player linked to `self`.
+       fun player(game: Game): Player is lazy do
+               var player = game.load_player(login)
+               if player == null then player = game.add_player(self)
+               return player
+       end
+end
+
+# A GameReactor reacts to event sent by a `Github::HookListener`.
+#
+# Subclasses of `GameReactor` are implemented to handle all kind of
+# `GithubEvent`.
+# Depending on the received event, the reactor is used to update game data.
+#
+# Reactors are mostly used with a `Github::HookListener` that dispatchs received
+# events from the Github API.
+#
+# Example:
+#
+# ~~~
+# import github::hooks
+#
+# # Reactor that prints received events in console.
+# class PrintReactor
+#      super GameReactor
+#
+#      redef fun react_event(game, e) do print e
+# end
+#
+# # Hook listener that redirect events to reactors.
+# class RpgHookListener
+#    super HookListener
+#
+#      redef fun apply_event(event) do
+#              var game = new Game(api, event.repo)
+#              var reactor = new PrintReactor
+#              reactor.react_event(game, event)
+#      end
+# end
+# ~~~
+#
+# See module `reactors` and `listener` for more examples.
+interface GameReactor
+
+       # Reacts to this `event` and update `game` accordingly.
+       #
+       # Concrete `GameReactor` implement this method to update game data
+       # for each specific GithubEvent.
+       #
+       # By default, only logs received events.
+       fun react_event(game: Game, event: GithubEvent) do
+               game.message(1, "Received event {event} for {game.repo.full_name}")
+       end
+end
+
+# utils
+
+# Sort players by descending number of nitcoins.
+#
+# The first in the list is the player with the more of nitcoins.
+class PlayerCoinComparator
+       super Comparator
+
+       redef type COMPARED: Player
+
+       redef fun compare(a, b) do return b.nitcoins <=> a.nitcoins
+end
diff --git a/contrib/nitrpg/src/listener.nit b/contrib/nitrpg/src/listener.nit
new file mode 100644 (file)
index 0000000..a77ec9f
--- /dev/null
@@ -0,0 +1,58 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This tool is runned to listen to `Github::Event` and update the game.
+module listener
+
+import statistics
+import reactors
+import github::hooks
+
+# `HookListener` that redirects events to a `Game` instance.
+class RpgHookListener
+   super HookListener
+
+       # Registered reactors list.
+       var reactors = new Array[GameReactor]
+
+       # Dispatch event to registered `reactors`.
+       redef fun apply_event(event) do
+               var game = new Game(api, event.repo)
+               # TODO handle verbosity with opts
+               game.verbose_lvl = 1
+               for reactor in reactors do reactor.react_event(game, event)
+       end
+end
+
+if args.length != 2 then
+       print "Error: missing argument"
+       print ""
+       print "Usage:"
+       print "listener <host> <port>"
+       exit 1
+end
+
+var host = args[0]
+var port = args[1].to_i
+
+var api = new GithubAPI(get_github_oauth)
+
+var listener = new RpgHookListener(api, host, port)
+listener.reactors.add new StatisticsReactor
+listener.reactors.add new PlayerReactor
+
+print "Listening events on {host}:{port}"
+listener.listen
diff --git a/contrib/nitrpg/src/reactors.nit b/contrib/nitrpg/src/reactors.nit
new file mode 100644 (file)
index 0000000..d1f3b3c
--- /dev/null
@@ -0,0 +1,67 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Various implementations of `GameReactor` can be found here.
+#
+# TODO This module use a lot of magic numbers for nitcoin rewards.
+# This should be extracted from configuration or stored elsewhere.
+module reactors
+
+import game
+
+# Reacts to event that can affect players (like giving nitcoins).
+class PlayerReactor
+       super GameReactor
+
+       redef fun react_event(game, e) do e.react_player_event(game)
+end
+
+redef class GithubEvent
+       # Reacts to a player related event.
+       #
+       # Called by `PlayerReactor::react_event`.
+       # No-op by default.
+       private fun react_player_event(game: Game) do end
+end
+
+redef class PullRequestEvent
+
+       # Rewards player for opened pull requests.
+       redef fun react_player_event(game) do
+               if action == "opened" then
+                       var player = pull.user.player(game)
+                       player.nitcoins += 10
+                       player.save
+               end
+       end
+end
+
+redef class IssueCommentEvent
+
+       # Rewards player for review comments.
+       #
+       # Actuallty we look if the comment contains the string `"+1"`.
+       #
+       # TODO only give nitcoins if reviewers < 2
+       redef fun react_player_event(game) do
+               # FIXME use a more precise way to locate reviews
+               if comment.body.has("\\+1\\b".to_re) then
+                       var player = comment.user.player(game)
+                       player.nitcoins += 2
+                       player.save
+               end
+       end
+end
diff --git a/contrib/nitrpg/src/statistics.nit b/contrib/nitrpg/src/statistics.nit
new file mode 100644 (file)
index 0000000..65d5a69
--- /dev/null
@@ -0,0 +1,153 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Statistics about the Game.
+#
+# This module uses `GameReactor` to extract statistics about the game from
+# triggered `Github::Event`.
+module statistics
+
+import game
+import github::hooks
+
+redef class Game
+
+       # Statistics for this game instance.
+       var stats = new GameStats
+
+       redef fun from_json(json) do
+               super
+               if json.has_key("statistics") then
+                       stats.from_json(json["statistics"].as(JsonObject))
+               end
+       end
+
+       redef fun to_json do
+               var obj = super
+               obj["statistics"] = stats.to_json
+               return obj
+       end
+
+       redef fun pretty do
+               var res = new FlatBuffer
+               res.append super
+               res.append "# stats:\n"
+               res.append stats.pretty
+               return res.write_to_string
+       end
+
+       redef fun clear do
+               super
+               stats.clear
+       end
+end
+
+# Game statistics structure that can be saved as a `GameEntity`.
+class GameStats
+       super GameEntity
+
+       redef var key = "statistics"
+
+       # Used internally to stats values.
+       private var stats = new HashMap[String, Int]
+
+       init do clear
+
+       # Load `self` from saved data
+       private fun from_json(json: JsonObject) do
+               for k, v in json do stats[k] = v.as(Int)
+       end
+
+       redef fun to_json do
+               var obj = new JsonObject
+               for k, v in stats do obj[k] = v
+               return obj
+       end
+
+       # Retrieves the current value of `key` statistic entry.
+       fun [](key: String): Int do return stats[key]
+
+       # Increments the value of `key` statistic entry by 1.
+       fun incr(key: String) do stats[key] += 1
+
+       # Decrements the value of `key` statistic entry by 1.
+       fun decr(key: String) do stats[key] -= 1
+
+       # Reset game stats.
+       fun clear do
+               stats["issues"] = 0
+               stats["issues_open"] = 0
+               stats["pulls"] = 0
+               stats["pulls_open"] = 0
+       end
+
+       redef fun pretty do
+               var res = new FlatBuffer
+               for k, v in stats do
+                       res.append "# {v} {k}\n"
+               end
+               return res.write_to_string
+       end
+end
+
+# `GameReactor` that computes statistics about the game.
+class StatisticsReactor
+       super GameReactor
+
+       redef fun react_event(game, e) do
+               super # log events
+               e.react_stats_event(game)
+               game.save
+       end
+end
+
+redef class GithubEvent
+       # Reacts to a statistics related event.
+       #
+       # Called by `StatisticsReactor::react_event`.
+       # No-op by default.
+       private fun react_stats_event(game: Game) do end
+end
+
+redef class IssuesEvent
+
+       # Count opened and closed issues.
+       redef fun react_stats_event(game) do
+               if action == "opened" then
+                       game.stats.incr("issues")
+                       game.stats.incr("issues_open")
+               else if action == "reopened" then
+                       game.stats.incr("issues_open")
+               else if action == "closed" then
+                       game.stats.decr("issues_open")
+               end
+       end
+end
+
+redef class PullRequestEvent
+
+       # Count opened and closed pull requests.
+       redef fun react_stats_event(game) do
+               if action == "opened" then
+                       game.stats.incr("pulls")
+                       game.stats.incr("pulls_open")
+               else if action == "reopened" then
+                       game.stats.incr("pulls_open")
+               else if action == "closed" then
+                       game.stats.decr("pulls_open")
+               end
+       end
+end
diff --git a/contrib/nitrpg/src/templates/panels.nit b/contrib/nitrpg/src/templates/panels.nit
new file mode 100644 (file)
index 0000000..03f3ea2
--- /dev/null
@@ -0,0 +1,305 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Panels templates for `nitpg`.
+module panels
+
+import templates_base
+
+# A panel can be displayed in a html page.
+#
+# This display a Bootstrap panel.
+class Panel
+       super Template
+
+       redef fun rendering do
+               add """<div class="panel panel-default">
+                           <div class="panel-heading">
+                                <h3 class="panel-title">"""
+               render_title
+               add """  </h3>
+                           </div>
+                          <div class="panel-body">"""
+               render_body
+               add """</div>
+                         </div>"""
+       end
+
+       # Render the panel title.
+       # Betweem `<h4>` tags.
+       fun render_title do end
+
+       # Render the panel body.
+       fun render_body do end
+end
+
+# A panel that contain only a table as body.
+class TablePanel
+       super Panel
+
+       redef fun rendering do
+               add """<div class="panel panel-default">
+                           <div class="panel-heading">
+                            <h3 class="panel-title">"""
+               render_title
+               add """
+                            </h3>
+                           </div>"""
+               render_body
+               add """</div>"""
+       end
+end
+
+# Display an error message within a panel.
+class ErrorPanel
+       super Panel
+
+       redef fun rendering do
+               add """
+<div class="panel panel-danger">
+       <div class="panel-heading">
+               <h3 class="panel-title">"""
+               render_title
+               add """
+               </h3>
+       </div>
+       <div class="panel-body">"""
+               render_body
+               add """
+       </div>
+</div>
+"""
+       end
+
+       # The error message to display as panel body.
+       var msg: String
+
+       redef fun render_title do
+               add "<span class=\"glyphicon glyphicon-warning-sign\"></span>&nbsp;&nbsp;"
+               add "Error"
+       end
+
+       redef fun render_body do
+               add msg.html_escape
+       end
+
+end
+
+# A panel that display repo statistics.
+class GameStatusPanel
+       super Panel
+
+       # Repo to display.
+       var game: Game
+
+       redef fun render_title do
+               add "<span class=\"glyphicon glyphicon-home\"></span>&nbsp;&nbsp;"
+               add "<a href=\"{game.url}\">{game.name}</a>"
+       end
+
+       redef fun render_body do
+               add "<strong class=\"text-success\">{game.load_players.length}</strong>"
+               add " <a href=\"{game.url}/players\">players</a><br>"
+               add "<strong class=\"text-success\">{game.stats["pulls"]}</strong> pull requests"
+               add " (<strong>{game.stats["pulls_open"]}</strong> open)<br>"
+               add "<strong class=\"text-success\">{game.stats["issues"]}</strong> issues"
+               add " (<strong>{game.stats["issues_open"]}</strong> open)<br>"
+       end
+end
+
+# Player status panel.
+class PlayerStatusPanel
+       super Panel
+
+       # Game instance.
+       var game: Game
+
+       # Target player.
+       var player: Player
+
+       redef fun render_title do
+               add "<a href=\"{player.url}\">"
+               add " <img class=\"img-circle\" style=\"width: 30px\""
+               add "   src=\"{player.user.avatar_url}\" alt=\"{player.name}\">"
+               add "</a>&nbsp;&nbsp;"
+               add "<a href=\"{player.url}\">{player.name}</a>"
+       end
+
+       redef fun render_body do
+               var ranking = game.player_ranking
+               # TODO player.rank
+               add "<p class=\"lead\">ranked "
+               add " <span class=\"text-success\"># {ranking[player.name]}</span></p>"
+               add "<strong class=\"text-success\">{player.nitcoins}</strong> nitcoins<br>"
+       end
+end
+
+# A panel that display a list of player in a repo.
+class ShortListPlayersPanel
+       super Panel
+
+       # Game instance.
+       var game: Game
+
+       redef fun render_title do
+               add "<span class=\"glyphicon glyphicon-user\"></span>&nbsp;&nbsp;"
+               add "<a href=\"{game.url}/players\">Players</a>"
+       end
+
+       redef fun render_body do
+               var players = game.load_players.values.to_a
+               if players.is_empty then
+                       add "<em>No player yet...</em>"
+                       return
+               end
+               (new PlayerCoinComparator).sort(players)
+               for player in players do
+                       add "<a href=\"{player.url}\">"
+                       add player.name
+                       add "</a>&nbsp;({player.nitcoins})<br>"
+               end
+       end
+end
+
+# A panel that display a list of player in a repo.
+class ListPlayersPanel
+       super TablePanel
+
+       # Game instance.
+       var game: Game
+
+       redef fun render_title do
+               add "<span class=\"glyphicon glyphicon-user\"></span>&nbsp;&nbsp;"
+               add "<a href=\"{game.url}/players\">Players</a>"
+       end
+
+       redef fun render_body do
+               var players = game.load_players.values.to_a
+               (new PlayerCoinComparator).sort(players)
+               if players.is_empty then
+                       add "<div class=\"panel-body\">"
+                       add "<em>No player yet...</em>"
+                       add "</div>"
+                       return
+               end
+               add """<table class="table table-striped table-hover">
+                           <tr>
+                                <th>#</th>
+                                <th>Player</th>
+                                <th>Nitcoins</th>
+                               </tr>"""
+               var rank = 1
+               for player in players do
+                       add "<tr>"
+                       add " <td>{rank}</td>"
+                       add " <td><a href=\"{player.url}\">{player.name}</a></td>"
+                       add " <td>{player.nitcoins}</td>"
+                       add "</tr>"
+                       rank += 1
+               end
+               add "</table>"
+       end
+end
+
+# A panel that display the podium.
+class PodiumPanel
+       super Panel
+
+       # Game instance.
+       var game: Game
+
+       redef fun render_title do
+               add "<span class=\"glyphicon glyphicon-stats\"></span>&nbsp;&nbsp;Hall of fame"
+       end
+
+       redef fun render_body do
+               var players = game.load_players.values.to_a
+               (new PlayerCoinComparator).sort(players)
+               if players.is_empty then
+                       add "<em>No players yet...</em>"
+                       return
+               end
+               add """
+                       <div class="container-fluid">
+                               <div id="podium" class="row row-sm-height">"""
+               var max = players.first.nitcoins
+               var orders = [3, 1, 0, 2, 4]
+               for order in orders do
+                       if order >= players.length then continue
+                       var player = players[order]
+                       var size = 0
+                       if max > 0 then size = player.nitcoins * 300 / max
+                       add """
+                               <div class="col-xs-2 col-xs-height col-xs-offset-{{{order}}} col-bottom"
+                                       style="text-align: center;">
+                                       <p>
+                                               <a href="{{{player.url}}}">
+                                                       <img class="img-circle" style="width: 80px"
+                                                               src="{{{player.user.avatar_url}}}" alt="{{{player.name}}}">
+                                               </a>
+                                       </p>
+                                       <p><a href="{{{player.url}}}">{{{player.name}}}</a></p>
+                                       <p>{{{player.nitcoins}}}</p>
+                                       <div class=" progress-bar-warning progress-bar-striped"
+                                               style="height: {{{size}}}px;"></div>
+                               </div>"""
+               end
+               add """
+                               </div>
+                       </div>"""
+       end
+end
+
+# A `Panel` that displays the list of PR to review for a `Player`.
+class PlayerReviewsPanel
+       super Panel
+
+       # Repo to display.
+       var game: Game
+
+       # Player to display customized list for.
+       var player: Player
+
+       redef fun render_title do
+               add "<span class=\"glyphicon glyphicon-check\"></span>&nbsp;&nbsp;"
+               add "Review pull requests to gain nitcoins!"
+       end
+
+       redef fun render_body do
+               var q = "is:open label:need_review sort:updated-asc " +
+                       "-involves:{player.name}"
+
+               var issues = game.repo.search_issues(q)
+               if issues.is_empty then
+                       add "<em>No pull request to review yet...</em>"
+                       return
+               end
+               for issue in issues do
+                       add """<div class="media">
+                               <a class="media-left" href="{{{player.url}}}">
+                                        <img class=\"img-circle\" style="width:50px"
+                                          src="{{{issue.user.avatar_url}}}" alt="{{{issue.user.login}}}">
+                                       </a>
+                                       <div class="media-body">
+                                        <h4 class="media-heading">
+                                        <a href="{{{issue.html_url}}}">#{{{issue.number}}} {{{issue.title}}}</a></h4>
+                                        <span class="text-muted">opened by </span>
+                                        <a href="{{{player.url}}}">{{{issue.user.login}}}</a>
+                                       </div>
+                                  </div>"""
+               end
+       end
+end
diff --git a/contrib/nitrpg/src/templates/templates.nit b/contrib/nitrpg/src/templates/templates.nit
new file mode 100644 (file)
index 0000000..f1affb9
--- /dev/null
@@ -0,0 +1,110 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Templates that compose the `nitrpg` site.
+module templates
+
+import panels
+
+# A page in the nitrp site.
+class NitRpgPage
+       super Template
+
+       # URL used as prefix for all the links generated in this page.
+       var root_url: String
+
+       # Breadcrumbs to this page if any.
+       var breadcrumbs: nullable Breadcrumbs = null is public writable
+
+       # Panels to display in the sidebar.
+       var side_panels = new Array[Panel]
+
+       # Panels to display in the page main container.
+       var flow_panels = new Array[Panel]
+
+       redef fun rendering do
+               render_header
+               render_footer
+       end
+
+       # Render the header shared by all pages.
+       fun render_header do
+               add """
+<!DOCTYPE html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+               <title>Github RPG</title>
+               <link rel="stylesheet"
+                       href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
+               <link rel="stylesheet" href="{{{root_url}}}/styles/main.css">
+       </head>
+       <body>
+               <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
+                       <a class="navbar-brand" href="/">Github RPG</a>"""
+               if not breadcrumbs == null then
+                       add breadcrumbs.as(not null)
+               end
+               add """
+               </nav>
+               <div class="container-fluid">
+                       <div class="row">"""
+               if not side_panels.is_empty then
+                       add """<div class="col-xs-3" id="side">"""
+                       for panel in side_panels do add panel
+                       add """</div>
+                                  <div class="col-xs-9" id="flow">"""
+               else
+                       add """<div class="col-xs-12" id="flow">"""
+               end
+               for panel in flow_panels do add panel
+               add """    </div>
+                       </div>
+               </div>
+"""
+       end
+
+       # Render the footer shared by all pages.
+       fun render_footer do
+               add """
+               <script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
+               <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
+       </body>
+</html>
+"""
+       end
+end
+
+# A Bootstrap breadcrumbs component.
+class Breadcrumbs
+       super Template
+
+       # Items to display in this breadcrumb.
+       var entries = new Array[String]
+
+       redef fun rendering do
+               add "<ol class=\"breadcrumb\">"
+               for entry in entries do
+                       add "<li>{entry}</li>"
+               end
+               add "</ol>"
+       end
+
+       # Add a link to the breadcrumbs.
+       fun add_link(href, name: String) do
+               entries.add "<a href=\"{href}\">{name}</a>"
+       end
+end
diff --git a/contrib/nitrpg/src/templates/templates_base.nit b/contrib/nitrpg/src/templates/templates_base.nit
new file mode 100644 (file)
index 0000000..adac329
--- /dev/null
@@ -0,0 +1,39 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Base HTML rendering templates for `nitpg`.
+module templates_base
+
+import statistics
+
+redef class GameEntity
+
+       # URL to this game entity page.
+       fun url: String do return game.url / key
+end
+
+redef class Game
+
+       # Root URL ise used as a prefix for `url`.
+       #
+       # This must be set before any access to `url`.
+       var root_url: String is noinit, writable
+
+       redef fun url do return "{root_url}/games" / key
+
+       # Displayed name.
+       fun name: String do return repo.full_name
+end
diff --git a/contrib/nitrpg/src/web.nit b/contrib/nitrpg/src/web.nit
new file mode 100644 (file)
index 0000000..80f7821
--- /dev/null
@@ -0,0 +1,168 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Display `nitrpg` data as a website.
+module web
+
+import nitcorn
+import templates
+
+# A custom action forn `nitrpg`.
+class RpgAction
+       super Action
+
+       # Root URL is used as a prefix for all URL generated by the actions.
+       var root_url: String
+
+       # Github oauth token used for GithubAPI.
+       var auth: String is lazy do return get_github_oauth
+
+       # API client used to import data from Github.
+       var api: GithubAPI is lazy do
+               var api = new GithubAPI(auth)
+               return api
+       end
+
+       init do
+               super
+               if auth.is_empty then
+                       print "Error: Invalid Github oauth token!"
+                       exit 1
+               end
+       end
+
+       # Return an Error reponse page.
+       fun bad_request(msg: String): HttpResponse do
+               var rsp = new HttpResponse(400)
+               var page = new NitRpgPage(root_url)
+               var error = new ErrorPanel(msg)
+               page.flow_panels.add error
+               rsp.body = page.write_to_string
+               return rsp
+       end
+end
+
+# An action that require a game.
+class GameAction
+       super RpgAction
+
+       # Response page stub.
+       var page: NitRpgPage is noinit
+
+       # Target game.
+       var game: Game is noinit
+
+       redef fun answer(request, url) is abstract
+
+       # Check errors and prepare response.
+       private fun prepare_response(request: HttpRequest, url: String): HttpResponse do
+               var owner = request.param("owner")
+               var repo_name = request.param("repo")
+               if owner == null or repo_name == null then
+                       var msg = "Bad request: should look like /repos/:owner/:repo."
+                       return bad_request(msg)
+               end
+               var repo = new Repo(api, "{owner}/{repo_name}")
+               game = new Game(api, repo)
+               game.root_url = root_url
+               if api.was_error then
+                       var msg = api.last_error.message
+                       return bad_request("Repo Error: {msg}")
+               end
+               var response = new HttpResponse(200)
+               page = new NitRpgPage(root_url)
+               page.side_panels.add new GameStatusPanel(game)
+               page.breadcrumbs = new Breadcrumbs
+               page.breadcrumbs.add_link(game.url, game.name)
+               return response
+       end
+end
+
+# Repo overview page.
+class RepoHome
+       super GameAction
+
+       redef fun answer(request, url) do
+               var rsp = prepare_response(request, url)
+               page.side_panels.add new ShortListPlayersPanel(game)
+               page.flow_panels.add new PodiumPanel(game)
+               rsp.body = page.write_to_string
+               return rsp
+       end
+end
+
+# Repo players list.
+class ListPlayers
+       super GameAction
+
+       redef fun answer(request, url) do
+               var rsp = prepare_response(request, url)
+               page.breadcrumbs.add_link(game.url / "players", "players")
+               page.flow_panels.add new ListPlayersPanel(game)
+               rsp.body = page.write_to_string
+               return rsp
+       end
+end
+
+# Player details page.
+class PlayerHome
+       super GameAction
+
+       redef fun answer(request, url) do
+               var rsp = prepare_response(request, url)
+               var name = request.param("player")
+               if name == null then
+                       var msg = "Bad request: should look like /:owner/:repo/:players/:name."
+                       return bad_request(msg)
+               end
+               var player = game.load_player(name)
+               if player == null then
+                       return bad_request("Request Error: unknown player {name}.")
+               end
+               page.breadcrumbs.add_link(game.url / "players", "players")
+               page.breadcrumbs.add_link(player.url, name)
+               page.side_panels.clear
+               page.side_panels.add new PlayerStatusPanel(game, player)
+               page.flow_panels.add new PlayerReviewsPanel(game, player)
+               rsp.body = page.write_to_string
+               return rsp
+       end
+end
+
+if args.length != 3 then
+       print "Error: missing argument"
+       print ""
+       print "Usage:"
+       print "web <host> <port> <root_url>"
+       exit 1
+end
+
+var host = args[0]
+var port = args[1]
+var root = args[2]
+
+var iface = "{host}:{port}"
+var vh = new VirtualHost(iface)
+vh.routes.add new Route("/styles/", new FileServer("www/styles"))
+vh.routes.add new Route("/games/:owner/:repo/players/:player", new PlayerHome(root))
+vh.routes.add new Route("/games/:owner/:repo/players", new ListPlayers(root))
+vh.routes.add new Route("/games/:owner/:repo", new RepoHome(root))
+
+var fac = new HttpFactory.and_libevent
+fac.config.virtual_hosts.add vh
+
+print "Launching server on http://{iface}/"
+fac.run
diff --git a/contrib/nitrpg/www/styles/main.css b/contrib/nitrpg/www/styles/main.css
new file mode 100644 (file)
index 0000000..865123e
--- /dev/null
@@ -0,0 +1,74 @@
+body { padding-top: 70px; }
+
+.navbar .breadcrumb {
+       background-color: transparent;
+       margin-bottom: 0;
+       margin-top: 0.5em;
+}
+
+/* columns of same height styles */
+.container-xs-height {
+    display:table;
+    padding-left:0px;
+    padding-right:0px;
+}
+.row-xs-height {
+    display:table-row;
+}
+.col-xs-height {
+    display:table-cell;
+    float:none;
+}
+@media (min-width: 768px) {
+    .container-sm-height {
+        display:table;
+        padding-left:0px;
+        padding-right:0px;
+    }
+    .row-sm-height {
+        display:table-row;
+    }
+    .col-sm-height {
+        display:table-cell;
+        float:none;
+    }
+}
+@media (min-width: 992px) {
+    .container-md-height {
+        display:table;
+        padding-left:0px;
+        padding-right:0px;
+    }
+    .row-md-height {
+        display:table-row;
+    }
+    .col-md-height {
+        display:table-cell;
+        float:none;
+    }
+}
+@media (min-width: 1200px) {
+    .container-lg-height {
+        display:table;
+        padding-left:0px;
+        padding-right:0px;
+    }
+    .row-lg-height {
+        display:table-row;
+    }
+    .col-lg-height {
+        display:table-cell;
+        float:none;
+    }
+}
+
+/* vertical alignment styles */
+.col-top {
+    vertical-align:top;
+}
+.col-middle {
+    vertical-align:middle;
+}
+.col-bottom {
+    vertical-align:bottom;
+}
index 0bf9560..1af06b7 100644 (file)
@@ -1909,15 +1909,22 @@ redef class Int
                end
        end
 
+       # C function to calculate the length of the `NativeString` to receive `self`
+       private fun int_to_s_len: Int is extern "native_int_length_str"
+
        # C function to convert an nit Int to a NativeString (char*)
-       private fun native_int_to_s: NativeString is extern "native_int_to_s"
+       private fun native_int_to_s(nstr: NativeString, strlen: Int) is extern "native_int_to_s"
 
        # return displayable int in base 10 and signed
        #
        #     assert 1.to_s            == "1"
        #     assert (-123).to_s       == "-123"
        redef fun to_s do
-               return native_int_to_s.to_s
+               var nslen = int_to_s_len
+               var ns = new NativeString(nslen + 1)
+               ns[nslen] = '\0'
+               native_int_to_s(ns, nslen + 1)
+               return ns.to_s_with_length(nslen)
        end
 
        # return displayable int in hexadecimal
@@ -2009,23 +2016,6 @@ redef class Float
 
                return p1 + "." + p2
        end
-
-       # `self` representation with `nb` digits after the '.'.
-       #
-       #     assert 12.345.to_precision_native(1) == "12.3"
-       #     assert 12.345.to_precision_native(2) == "12.35"
-       #     assert 12.345.to_precision_native(3) == "12.345"
-       #     assert 12.345.to_precision_native(4) == "12.3450"
-       fun to_precision_native(nb: Int): String import NativeString.to_s `{
-               int size;
-               char *str;
-
-               size = snprintf(NULL, 0, "%.*f", (int)nb, recv);
-               str = malloc(size + 1);
-               sprintf(str, "%.*f", (int)nb, recv );
-
-               return NativeString_to_s( str );
-       `}
 end
 
 redef class Char
index 1a11e99..686d6be 100644 (file)
 
 #include "string_nit.h"
 
+// Returns the length of `recv` as a `char*` (excluding the null character)
+long native_int_length_str(long recv){
+       return snprintf(NULL, 0, "%ld", recv);
+}
+
 // Integer to NativeString method
-char* native_int_to_s(long recv){
-       int len = snprintf(NULL, 0, "%ld", recv);
-       char* str = malloc(len+1);
-       sprintf(str, "%ld", recv);
-       return str;
+void native_int_to_s(long recv, char* str, long buflen){
+       snprintf(str, buflen, "%ld", recv);
 }
index 311041d..f33580f 100644 (file)
@@ -13,6 +13,7 @@
  * another product.
  */
 
-char* native_int_to_s(long recv);
+long native_int_length_str(long recv);
+void native_int_to_s(long recv, char* str, long buflen);
 
 #endif
index 09e8389..e0b3867 100644 (file)
@@ -135,7 +135,9 @@ extern class Tm `{struct tm *`}
                c_format = String_to_cstring(format);
 
                res = strftime(buf, 100, c_format, recv);
-               return NativeString_to_s(buf);
+               String s = NativeString_to_s_with_copy(buf);
+               free(buf);
+               return s;
        `}
 
        redef fun to_s do return asctime.replace("\n", "")
index 8c9742c..eb02ada 100644 (file)
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-NITCOPT=
+NITCOPT=--semi-global
 OLDNITCOPT=
 OBJS=nitc nitpick nit nitdoc nitls nitunit nitpretty nitmetrics nitx nitlight nitdbg_client nitserial
 SRCS=$(patsubst %,%.nit,$(OBJS))
index 07a8ede..f286f02 100644 (file)
@@ -856,6 +856,12 @@ extern void nitni_global_ref_decr( struct nitni_ref *ref );
 
                v.add("return 0;")
                v.add("\}")
+
+               for m in mainmodule.in_importation.greaters do
+                       var f = "FILE_"+m.c_name
+                       v.add "const char {f}[] = \"{m.location.file.filename.escape_to_c}\";"
+                       provide_declaration(f, "extern const char {f}[];")
+               end
        end
 
        # Copile all C functions related to the [incr|decr]_ref features of the FFI
@@ -1543,8 +1549,11 @@ abstract class AbstractCompilerVisitor
 
        fun add_raw_abort
        do
-               if self.current_node != null and self.current_node.location.file != null then
-                       self.add("PRINT_ERROR(\" (%s:%d)\\n\", \"{self.current_node.location.file.filename.escape_to_c}\", {current_node.location.line_start});")
+               if self.current_node != null and self.current_node.location.file != null and
+                               self.current_node.location.file.mmodule != null then
+                       var f = "FILE_{self.current_node.location.file.mmodule.c_name}"
+                       self.require_declaration(f)
+                       self.add("PRINT_ERROR(\" (%s:%d)\\n\", {f}, {current_node.location.line_start});")
                else
                        self.add("PRINT_ERROR(\"\\n\");")
                end
index 19443b4..a6fb971 100644 (file)
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Nitdoc page generation
+# Nitdoc generation framework.
 module doc
 
-import doc_pages
-
+import doc_base
+import doc_phases
diff --git a/src/doc/doc_base.nit b/src/doc/doc_base.nit
new file mode 100644 (file)
index 0000000..56b57c2
--- /dev/null
@@ -0,0 +1,219 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Base entities shared by all the nitdoc code.
+module doc_base
+
+import toolcontext
+import doc_model # FIXME maintain compatibility with old templates.
+
+# The model of a Nitdoc documentation.
+#
+# `DocModel` contains the list of the `DocPage` to be generated.
+#
+# The model is populated through `DocPhase` to be constructed.
+# It is a placeholder to share data between each phase.
+class DocModel
+
+       # `DocPage` composing the documentation.
+       #
+       # This is where `DocPhase` store and access pages to produce documentation.
+       var pages = new Array[DocPage]
+
+       # Nit `Model` from which we extract the documentation.
+       var model: Model is writable
+
+       # The entry point of the `model`.
+       var mainmodule: MModule is writable
+end
+
+# A documentation page abstraction.
+#
+# The page contains a link to the `root` of the `DocComposite` that compose the
+# the page.
+class DocPage
+
+       # Title of this page.
+       var title: String is writable
+
+       # Root element of the page.
+       #
+       # `DocPhase` access the structure of the page from the `DocRoot`.
+       var root = new DocRoot
+
+       redef fun to_s do return title
+end
+
+# `DocPage` elements that can be nested in another.
+#
+# `DocComposite` is an abstraction for everything that go in a `DocPage` like
+# sections, articles, images, lists, graphs...
+#
+# It provides base services for nesting mechanisms following the
+# *Composite pattern*.
+# The composed structure is a tree from a `DocRoot` that can be manipulated
+# recursively.
+abstract class DocComposite
+
+       # Parent element.
+       var parent: nullable DocComposite = null
+
+       # Does `self` have a `parent`?
+       fun is_root: Bool do return parent == null
+
+       # Children elements contained in `self`.
+       #
+       # Children are ordered, this order can be changed by the `DocPhase`.
+       var children = new Array[DocComposite]
+
+       # Does `self` have `children`?
+       fun is_empty: Bool do return children.is_empty
+
+       # Add a `child` to `self`.
+       #
+       # Shortcut for `children.add`.
+       fun add(child: DocComposite) do children.add child
+end
+
+# The `DocComposite` element that contains all the other.
+#
+# The root uses a specific subclass to provide different a different behavior
+# than other `DocComposite` elements.
+class DocRoot
+       super DocComposite
+
+       # No op for `RootSection`.
+       redef fun parent=(p) do end
+end
+
+# Base page elements.
+
+# `DocSection` are used to break the documentation page into meaningfull parts.
+#
+# The content of the documentation summary is based on the section structure
+# contained in the DocComposite tree.
+class DocSection
+       super DocComposite
+end
+
+# `DocArticle` are pieces of documentation.
+#
+# They maintains the content (text, list, image...) of a documentation page.
+class DocArticle
+       super DocComposite
+end
+
+# A DocPhase is a step in the production of a Nitdoc documentation.
+#
+# Phases work from a `DocModel`.
+# Specific phases are used to populate, organize, enhance and render the content
+# of the documentation pages.
+#
+# See `doc_phases` for available DocPhase.
+class DocPhase
+
+       # Link to the ToolContext to access Nitdoc tool options.
+       var ctx: ToolContext
+
+       # `DocModel` used by this phase to work.
+       var doc: DocModel
+
+       # Starting point of a `DocPhase`.
+       #
+       # This is where the behavior of the phase is implemented.
+       # Phases can populate, edit or render the `doc` from here.
+       fun apply is abstract
+end
+
+redef class ToolContext
+
+       # Directory where the Nitdoc is rendered.
+       var opt_dir = new OptionString("output directory", "-d", "--dir")
+
+       # Shortcut for `opt_dir.value` with default "doc".
+       var output_dir: String is lazy do return opt_dir.value or else "doc"
+
+       redef init do
+               super
+               option_context.add_option(opt_dir)
+       end
+end
+
+# Catalog properties by kind.
+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
+
+# An ordered list of properties of the same kind.
+class PropertyGroup[E: MProperty]
+       super Array[E]
+
+       # The title of the group, as displayed to the user.
+       var title: String
+end
index 2c80ffb..99691d1 100644 (file)
@@ -111,7 +111,7 @@ redef class MEntity
                return new TplListItem.with_content(lnk)
        end
 
-       fun tpl_css_classes: Array[String] is abstract
+       var tpl_css_classes = new Array[String]
 
        # Box title for this mentity
        fun tpl_title: Template do
@@ -257,6 +257,7 @@ redef class MClass
        redef fun mdoc_or_fallback do return intro.mdoc
 
        redef fun tpl_declaration do return intro.tpl_declaration
+       redef fun tpl_definition do return intro.tpl_definition
 
        redef fun tpl_namespace do
                var tpl = new Template
@@ -365,7 +366,6 @@ redef class MClassDef
 
        redef fun tpl_definition do
                var tpl = new TplClassDefinition
-               tpl.namespace = tpl_namespace
                var mdoc = mdoc_or_fallback
                if mdoc != null then
                        tpl.comment = mdoc.tpl_comment
@@ -449,7 +449,6 @@ redef class MPropDef
 
        redef fun tpl_definition do
                var tpl = new TplDefinition
-               tpl.namespace = mclassdef.tpl_namespace
                var mdoc = mdoc_or_fallback
                if mdoc != null then
                        tpl.comment = mdoc.tpl_comment
@@ -556,6 +555,7 @@ end
 redef class MVirtualTypeDef
        redef fun tpl_signature do
                var tpl = new Template
+               if bound == null then return tpl
                tpl.add ": "
                tpl.add bound.tpl_signature
                return tpl
@@ -705,7 +705,6 @@ redef class MInnerClassDef
 
        redef fun tpl_definition do
                var tpl = new TplClassDefinition
-               tpl.namespace = mclassdef.tpl_namespace
                var mdoc = mdoc_or_fallback
                if mdoc != null then
                        tpl.comment = mdoc.tpl_comment
diff --git a/src/doc/doc_pages.nit b/src/doc/doc_pages.nit
deleted file mode 100644 (file)
index 38e6cdf..0000000
+++ /dev/null
@@ -1,1603 +0,0 @@
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Nitdoc page generation
-module doc_pages
-
-import toolcontext
-import doc_model
-private import json::static
-
-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")
-
-       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")
-       private var opt_custom_intro = new OptionString("custom intro text for homepage", "--custom-overview-text")
-       private var opt_custom_footer = new OptionString("custom footer text", "--custom-footer-text")
-
-       private var opt_github_upstream = new OptionString("Git branch where edited commits will be pulled into (ex: user:repo:branch)", "--github-upstream")
-       private var opt_github_base_sha1 = new OptionString("Git sha1 of base commit used to create pull request", "--github-base-sha1")
-       private var opt_github_gitdir = new OptionString("Git working directory used to resolve path name (ex: /home/me/myproject/)", "--github-gitdir")
-
-       private var opt_piwik_tracker = new OptionString("Piwik tracker URL (ex: nitlanguage.org/piwik/)", "--piwik-tracker")
-       private var opt_piwik_site_id = new OptionString("Piwik site ID", "--piwik-site-id")
-
-       private var output_dir: String
-       private var min_visibility: MVisibility
-
-       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)
-
-               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
-               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
-               if not gh_upstream == null or not gh_base_sha == null or not gh_gitdir == null then
-                       if gh_upstream == null or gh_base_sha == null or gh_gitdir == null then
-                               print "Error: Options {opt_github_upstream.names.first}, {opt_github_base_sha1.names.first} and {opt_github_gitdir.names.first} are required to enable the GitHub plugin"
-                               abort
-                       end
-               end
-       end
-
-       # 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 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
-               if not output_dir.file_exists then output_dir.mkdir
-               # locate share dir
-               var sharedir = ctx.opt_sharedir.value
-               if sharedir == null then
-                       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 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.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 or mmodule.is_test_suite 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 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(ctx, model)
-               quicksearch.render.write_to_file("{ctx.output_dir.to_s}/quicksearch-list.js")
-       end
-end
-
-# Nitdoc QuickSearch list generator
-#
-# Create a JSON object containing links to:
-#  * modules
-#  * mclasses
-#  * mpropdefs
-# All entities are grouped by name to make the research easier.
-class QuickSearch
-
-       private var table = new QuickSearchTable
-
-       var ctx: ToolContext
-       var model: Model
-
-       init do
-               for mmodule in model.mmodules do
-                       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 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 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
-               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
-               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: ToolContext
-       private var model: Model
-       private var mainmodule: MModule
-       private var name_sorter = new MEntityNameSorter
-
-       # Render the page as a html template
-       fun render: Template do
-               var shareurl = "."
-               if ctx.opt_shareurl.value != null then
-                       shareurl = ctx.opt_shareurl.value.as(not null)
-               end
-
-               # build page
-               var tpl = tpl_page
-               tpl.title = tpl_title
-               tpl.url = page_url
-               tpl.shareurl = shareurl
-               tpl.topmenu = tpl_topmenu
-               tpl_content
-               tpl.footer = ctx.opt_custom_footer.value
-               tpl.body_attrs.add(new TagAttribute("data-bootstrap-share", shareurl))
-               tpl.sidebar = tpl_sidebar
-
-               # piwik tracking
-               var tracker_url = ctx.opt_piwik_tracker.value
-               var site_id = ctx.opt_piwik_site_id.value
-               if tracker_url != null and site_id != null then
-                       tpl.scripts.add new TplPiwikScript(tracker_url, site_id)
-               end
-               return tpl
-       end
-
-       # URL to this page.
-       fun page_url: String is abstract
-
-       # Build page template
-       fun tpl_page: TplPage is abstract
-
-       # Build page sidebar if any
-       fun tpl_sidebar: nullable TplSidebar do return null
-
-       # Build page title string
-       fun tpl_title: String do
-               if ctx.opt_custom_title.value != null then
-                       return ctx.opt_custom_title.value.to_s
-               end
-               return "Nitdoc"
-       end
-
-       # Build top menu template
-       fun tpl_topmenu: TplTopMenu do
-               var topmenu = new TplTopMenu(page_url)
-               var brand = ctx.opt_custom_brand.value
-               if brand != null then
-                       var tpl = new Template
-                       tpl.add "<span class='navbar-brand'>"
-                       tpl.add brand
-                       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
-
-       # Build page content template
-       fun tpl_content is abstract
-
-       # Clickable graphviz image using dot format
-       # return null if no graph for this page
-       fun tpl_graph(dot: Buffer, name: String, title: nullable String): nullable TplArticle do
-               if ctx.opt_nodot.value then return null
-               var output_dir = ctx.output_dir
-               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 {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("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
-               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
-       end
-
-       # A (source) link template for a given location
-       fun tpl_showsource(location: nullable Location): nullable String
-       do
-               if location == null then return null
-               var source = ctx.opt_source.value
-               if source == null then
-                       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)
-               x = source.split_with("%l")
-               source = x.join(location.line_start.to_s)
-               x = source.split_with("%L")
-               source = x.join(location.line_end.to_s)
-               source = source.simplify_path
-               return "<a target='_blank' title='Show source' href=\"{source.to_s.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 = mclass.tpl_article
-               if not mclassdefs.has(mclass.intro) then
-                       # add intro synopsys
-                       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 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
-               end
-               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
-       #
-       # `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 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.nitdoc_name,
-                                       "Go to introduction")
-                       title.add "redef "
-                       title.add lnk
-               end
-               article.title = title
-               article.title_classes.add "signature"
-               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
-               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 != null then
-                                       redef_content.add mpropdef.mdoc.tpl_comment
-                               end
-                               redef_article.content = redef_content
-                               subarticle.add_child redef_article
-                       end
-               end
-               # 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
-
-# The overview page
-# Display a list of modules contained in program
-class NitdocOverview
-       super NitdocPage
-
-       private var page = new TplPage
-       redef fun tpl_page do return page
-
-       private var sidebar = new TplSidebar
-       redef fun tpl_sidebar do return sidebar
-
-       redef fun tpl_title do
-               if ctx.opt_custom_title.value != null then
-                       return ctx.opt_custom_title.value.to_s
-               else
-                       return "Overview"
-               end
-       end
-
-       redef fun page_url do return "index.html"
-
-       # intro text
-       private fun tpl_intro: TplSection do
-               var section = new TplSection.with_title("overview", tpl_title)
-               var article = new TplArticle("intro")
-               if ctx.opt_custom_intro.value != null then
-                       article.content = ctx.opt_custom_intro.value.to_s
-               end
-               section.add_child article
-               return section
-       end
-
-       # projects list
-       private fun tpl_projects(section: TplSection) do
-               # Projects list
-               var mprojects = model.mprojects.to_a
-               var sorter = new MConcernRankSorter
-               sorter.sort mprojects
-               var ssection = new TplSection.with_title("projects", "Projects")
-               for mproject in mprojects do
-                       ssection.add_child tpl_mproject_article(mproject)
-               end
-               section.add_child ssection
-       end
-
-       redef fun tpl_content do
-               var top = tpl_intro
-               tpl_projects(top)
-               tpl_page.add_section top
-       end
-end
-
-# The search page
-# Display a list of modules, classes and properties
-class NitdocSearch
-       super NitdocPage
-
-       private var page = new TplPage
-       redef fun tpl_page do return page
-
-       redef fun tpl_title do return "Index"
-
-       redef fun page_url do return "search.html"
-
-       redef fun tpl_content do
-               var tpl = new TplSearchPage("search_all")
-               var section = new TplSection("search")
-               # title
-               tpl.title = "Index"
-               # modules list
-               for mmodule in modules_list do
-                       tpl.modules.add mmodule.tpl_link
-               end
-               # classes list
-               for mclass in classes_list do
-                       tpl.classes.add mclass.tpl_link
-               end
-               # properties list
-               for mproperty in mprops_list do
-                       var m = new Template
-                       m.add mproperty.intro.tpl_link
-                       m.add " ("
-                       m.add mproperty.intro.mclassdef.mclass.tpl_link
-                       m.add ")"
-                       tpl.props.add m
-               end
-               section.add_child tpl
-               tpl_page.add_section section
-       end
-
-       # Extract mmodule list to display (sorted by name)
-       private fun modules_list: Array[MModule] do
-               var sorted = new Array[MModule]
-               for mmodule in model.mmodule_importation_hierarchy do
-                       if mmodule.is_fictive or mmodule.is_test_suite then continue
-                       sorted.add mmodule
-               end
-               name_sorter.sort(sorted)
-               return sorted
-       end
-
-       # Extract mclass list to display (sorted by name)
-       private fun classes_list: Array[MClass] do
-               var sorted = new Array[MClass]
-               for mclass in model.mclasses do
-                       if not ctx.filter_mclass(mclass) then continue
-                       sorted.add mclass
-               end
-               name_sorter.sort(sorted)
-               return sorted
-       end
-
-       # Extract mproperty list to display (sorted by name)
-       private fun mprops_list: Array[MProperty] do
-               var sorted = new Array[MProperty]
-               for mproperty in 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 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
-       redef fun tpl_page do return page
-
-       private var sidebar = new TplSidebar
-       redef fun tpl_sidebar do return sidebar
-
-       redef fun tpl_title do return mmodule.nitdoc_name
-       redef fun page_url do return mmodule.nitdoc_url
-
-       redef fun tpl_topmenu do
-               var topmenu = super
-               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("top", tpl_title)
-               section.subtitle = mmodule.tpl_declaration
-
-               var article = new TplArticle("intro")
-               var def = mmodule.tpl_definition
-               var location = mmodule.location
-               article.source_link = tpl_showsource(location)
-               article.content = def
-               section.add_child article
-               return section
-       end
-
-       # inheritance section
-       private fun tpl_inheritance(parent: TplSection) do
-               # Extract relevent modules
-               var 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("dependencies", "Dependencies")
-
-               # Graph
-               var mmodules = new HashSet[MModule]
-               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
-
-               # Imports
-               var lst = new Array[MModule]
-               for dep in imports do
-                       if dep.is_fictive or dep.is_test_suite then continue
-                       if dep == mmodule then continue
-                       lst.add(dep)
-               end
-               if not lst.is_empty then
-                       name_sorter.sort lst
-                       section.add_child tpl_list("imports", "Imports", lst)
-               end
-
-               # Clients
-               lst = new Array[MModule]
-               for dep in clients do
-                       if dep.is_fictive or dep.is_test_suite then continue
-                       if dep == mmodule then continue
-                       lst.add(dep)
-               end
-               if not lst.is_empty then
-                       name_sorter.sort lst
-                       section.add_child tpl_list("clients", "Clients", lst)
-               end
-
-               parent.add_child section
-       end
-
-       private fun tpl_list(id: String, title: String, mmodules: Array[MModule]): TplArticle do
-               var article = new TplArticle.with_title(id, title)
-               var list = new TplList.with_classes(["list-unstyled", "list-definition"])
-               for mmodule in mmodules do list.elts.add mmodule.tpl_list_item
-               article.content = list
-               return article
-       end
-
-       private fun tpl_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
-               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
-       end
-
-       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
-               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
-
-       # Genrate dot hierarchy for class inheritance
-       fun tpl_dot(mmodules: Collection[MModule]): nullable TplArticle do
-               var poset = new POSet[MModule]
-               for mmodule in mmodules do
-                       if mmodule.is_fictive or mmodule.is_test_suite then continue
-                       poset.add_node mmodule
-                       for omodule in mmodules do
-                               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)
-                               end
-                       end
-               end
-               # build graph
-               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.escape_to_dot}\"[shape=box,margin=0.03];\n")
-                       else
-                               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.escape_to_dot}\"->\"{omodule.name.escape_to_dot}\";\n")
-                       end
-               end
-               op.append("\}\n")
-               return tpl_graph(op, name, null)
-       end
-
-       private fun sort_by_mclass(mclassdefs: Collection[MClassDef]): Map[MClass, Set[MClassDef]] do
-               var map = new HashMap[MClass, Set[MClassDef]]
-               for mclassdef in mclassdefs do
-                       var mclass = mclassdef.mclass
-                       if not map.has_key(mclass) then map[mclass] = new HashSet[MClassDef]
-                       map[mclass].add mclassdef
-               end
-               return map
-       end
-end
-
-# A class page
-# Display a list properties defined or redefined for this class
-class NitdocClass
-       super NitdocPage
-
-       private var mclass: MClass
-       private var concerns: ConcernsTree is noinit
-       private var mprops2mdefs: Map[MProperty, Set[MPropDef]] is noinit
-       private var mmodules2mprops: Map[MModule, Set[MProperty]] is noinit
-
-       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)
-               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
-       redef fun tpl_page do return page
-
-       private var sidebar = new TplSidebar
-       redef fun tpl_sidebar do return sidebar
-
-       redef fun tpl_title do return "{mclass.nitdoc_name}{mclass.tpl_signature.write_to_string}"
-       redef fun page_url do return mclass.nitdoc_url
-
-       redef fun tpl_topmenu do
-               var topmenu = super
-               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 by_kind = new PropertiesByKind.with_elements(mclass_inherited_mprops)
-               var summary = new TplList.with_classes(["list-unstyled"])
-
-               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(mprops: PropertyGroup[MProperty], summary: TplList) do
-               if mprops.is_empty then return
-               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)
-               end
-               entry.append list
-               summary.elts.add entry
-       end
-
-       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 cls_url = mprop.intro.mclassdef.mclass.nitdoc_url
-                       var def_url = "{cls_url}#{mprop.nitdoc_id}"
-                       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
-                       return new TplListItem.with_content(item)
-               end
-               var defs = mprops2mdefs[mprop]
-               if defs.has(mprop.intro) then
-                       classes.add "intro"
-               else
-                       classes.add "redef"
-               end
-               var lnk = new Template
-               lnk.add new TplLabel.with_classes(classes)
-               lnk.add mprop.tpl_anchor
-               return new TplListItem.with_content(lnk)
-       end
-
-       private fun tpl_intro: TplSection do
-               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(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(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(mainmodule).greaters do
-                       if c == mclass 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(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(mainmodule).smallers do
-                       if c == mclass then continue
-                       if not ctx.filter_mclass(c) then continue
-                       if hchildren.has(c) then continue
-                       hdescendants.add c
-               end
-
-               # Display lists
-               var section = new TplSection.with_title("inheritance", "Inheritance")
-
-               # Graph
-               var mclasses = new HashSet[MClass]
-               mclasses.add_all hancestors
-               mclasses.add_all hparents
-               mclasses.add_all hchildren
-               mclasses.add_all hdescendants
-               mclasses.add mclass
-               var graph = tpl_dot(mclasses)
-               if graph != null then section.add_child graph
-
-               # parents
-               if not hparents.is_empty then
-                       var lst = hparents.to_a
-                       name_sorter.sort lst
-                       section.add_child tpl_list("parents", "Parents", lst)
-               end
-
-               # ancestors
-               if not hancestors.is_empty then
-                       var lst = hancestors.to_a
-                       name_sorter.sort lst
-                       section.add_child tpl_list("ancestors", "Ancestors", lst)
-               end
-
-               # children
-               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 then
-                       var lst = hdescendants.to_a
-                       name_sorter.sort lst
-                       section.add_child tpl_list("descendants", "Descendants", lst)
-               end
-
-               parent.add_child section
-       end
-
-       private fun tpl_list(id: String, title: String, elts: Array[MClass]): TplArticle do
-               var article = new TplArticle.with_title(id, title)
-               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 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 = mmodules2mprops[mentity]
-                               var by_kind = new PropertiesByKind.with_elements(mprops)
-
-                               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(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
-                       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_inheritance(top)
-               tpl_concerns(top)
-               tpl_properties(top)
-               tpl_page.add_section top
-       end
-
-       private fun sort_by_mproperty(mpropdefs: Collection[MPropDef]): Map[MProperty, Set[MPropDef]] do
-               var map = new HashMap[MProperty, Set[MPropDef]]
-               for mpropdef in mpropdefs do
-                       var mproperty = mpropdef.mproperty
-                       if not map.has_key(mproperty) then map[mproperty] = new HashSet[MPropDef]
-                       map[mproperty].add mpropdef
-               end
-               return map
-       end
-
-       private fun sort_by_mmodule(mprops: Collection[MProperty]): Map[MModule, Set[MProperty]] do
-               var map = new HashMap[MModule, Set[MProperty]]
-               for mprop in mprops do
-                       var mpropdefs = mprops2mdefs[mprop].to_a
-                       mainmodule.linearize_mpropdefs(mpropdefs)
-                       var mmodule = mpropdefs.first.mclassdef.mmodule
-                       if not map.has_key(mmodule) then map[mmodule] = new HashSet[MProperty]
-                       map[mmodule].add mprop
-               end
-               return map
-       end
-
-       private fun 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(mainmodule, ctx.min_visibility) do
-                       if local.has(mprop) then continue
-                       #if mprop isa MMethod and mprop.is_init then continue
-                       if mprop.intro.mclassdef.mclass.name == "Object" and
-                               (mprop.visibility == protected_visibility or
-                               mprop.intro.mclassdef.mmodule.name != "kernel") then continue
-                       res.add mprop
-               end
-               res.add_all local
-               return res
-       end
-
-       private fun collect_mmodules(mprops: Collection[MProperty]): Set[MModule] do
-               var res = new HashSet[MModule]
-               for mprop in mprops do
-                       if mprops2mdefs.has_key(mprop) then
-                               for mpropdef in mprops2mdefs[mprop] do res.add mpropdef.mclassdef.mmodule
-                       end
-               end
-               return res
-       end
-
-       # Generate dot hierarchy for classes
-       fun tpl_dot(mclasses: Collection[MClass]): nullable TplArticle do
-               var poset = new POSet[MClass]
-
-               for mclass in mclasses do
-                       poset.add_node mclass
-                       for oclass in mclasses do
-                               if mclass == oclass then continue
-                               poset.add_node oclass
-                               if mclass.in_hierarchy(mainmodule) < oclass then
-                                       poset.add_edge(mclass, oclass)
-                               end
-                       end
-               end
-
-               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.escape_to_dot}\"[shape=box,margin=0.03];\n")
-                       else
-                               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.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, 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
-
diff --git a/src/doc/doc_phases/doc_concerns.nit b/src/doc/doc_phases/doc_concerns.nit
new file mode 100644 (file)
index 0000000..d1ada76
--- /dev/null
@@ -0,0 +1,149 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Concerns computation.
+module doc_concerns
+
+import doc_pages
+
+# ConcernsPhase computes the ConcernsTree used for each page layout.
+class ConcernsPhase
+       super DocPhase
+
+       # Populates the given DocModel.
+       redef fun apply do
+               for page in doc.pages do page.build_concerns(doc)
+       end
+end
+
+redef class DocPage
+
+       # Build the `concerns` tree for this page.
+       #
+       # Since only `MEntityPage`, this method is a no-op for everything else.
+       private fun build_concerns(doc: DocModel) do end
+end
+
+redef class MEntityPage
+
+       # Concerns to display in this page.
+       var concerns: nullable ConcernsTree = null
+end
+
+# TODO ConcernsTrees are a PITA, following redef should not be needed here...
+# The bad, so baaaadddd, ConcernsTree interface induces a lot of useless code
+# in all phases.
+
+redef class MGroupPage
+
+       # Introduced classes in `mentity` that should appear in this page.
+       var intros = new HashSet[MClass]
+
+       # Refined classes in `mentity` that should appear in this page.
+       var redefs = new HashSet[MClass]
+
+       redef fun build_concerns(doc) do
+               var mmodules = new HashSet[MModule]
+               for mmodule in mentity.collect_mmodules do
+                       if doc.mmodules.has(mmodule) then mmodules.add mmodule
+                       # collect mclasses
+                       for mclass in mmodule.intro_mclasses do
+                               if doc.mclasses.has(mclass) then intros.add mclass
+                       end
+                       for mclass in mmodule.redef_mclasses do
+                               if doc.mclasses.has(mclass) then redefs.add mclass
+                       end
+               end
+               concerns = doc.model.concerns_tree(mmodules)
+       end
+end
+
+redef class MModulePage
+
+       # MClasses defined in `mentity` to display in this page.
+       var mclasses = new HashSet[MClass]
+
+       # MClassDefs located in `mentity` to display in this page.
+       var mclassdefs = new HashSet[MClassDef]
+
+       redef fun build_concerns(doc) do
+               # extract mclassdefs in mmodule
+               for mclassdef in mentity.mclassdefs do
+                       if doc.mclassdefs.has(mclassdef) then mclassdefs.add mclassdef
+               end
+               # extract mclasses in mmodule
+               for mclassdef in mclassdefs do
+                       var mclass = mclassdef.mclass
+                       if doc.mclasses.has(mclass) then mclasses.add mclass
+               end
+               # extract concerns
+               var mods = new HashSet[MModule]
+               for mclass in mclasses do
+                       var mod = mclass.intro_mmodule
+                       if doc.mmodules.has(mod) then mods.add mod
+               end
+               concerns = doc.model.concerns_tree(mods)
+       end
+end
+
+redef class MClassPage
+
+       # MClassDefs to display in this page.
+       var mclassdefs = new HashSet[MClassDef]
+
+       # MPropdefs to display in this page.
+       var mpropdefs = new HashSet[MPropDef]
+
+       redef fun build_concerns(doc) do
+               # collect mclassdefs
+               for mclassdef in mentity.mclassdefs do
+                       if doc.mclassdefs.has(mclassdef) then mclassdefs.add mclassdef
+               end
+               # collect mpropdefs
+               for mclassdef in mclassdefs do
+                       for mpropdef in mclassdef.mpropdefs do
+                               if doc.mpropdefs.has(mpropdef) then mpropdefs.add mpropdef
+                       end
+               end
+               # collect concerns
+               var mods = new HashSet[MModule]
+               for mpropdef in mpropdefs do
+                       var mod = mpropdef.mclassdef.mmodule
+                       if doc.mmodules.has(mod) then mods.add mod
+               end
+               concerns = doc.model.concerns_tree(mods)
+       end
+end
+
+redef class MPropertyPage
+
+       # MPropdefs to display in this page.
+       var mpropdefs = new HashSet[MPropDef]
+
+       redef fun build_concerns(doc) do
+               # collect mpropdefs
+               for mpropdef in mentity.mpropdefs do
+                       # FIXME diff hack
+                       if mpropdef.is_intro then continue
+                       if doc.mpropdefs.has(mpropdef) then mpropdefs.add mpropdef
+               end
+               # collect concerns
+               var mods = new HashSet[MModule]
+               for mpropdef in mpropdefs do
+                       var mod = mpropdef.mclassdef.mmodule
+                       if doc.mmodules.has(mod) then mods.add mod
+               end
+               concerns = doc.model.concerns_tree(mods)
+       end
+end
diff --git a/src/doc/doc_phases/doc_extract.nit b/src/doc/doc_phases/doc_extract.nit
new file mode 100644 (file)
index 0000000..53adeac
--- /dev/null
@@ -0,0 +1,149 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Extract data mentities of Model that will be documented.
+#
+# ExtractionPhase populates the DocModel that is the base for all other phases.
+# No DocPages are created at this level.
+#
+# TODO build a model instead?
+module doc_extract
+
+import doc_base
+
+redef class ToolContext
+
+       # Do not generate documentation for attributes.
+       var opt_no_attributes = new OptionBool("ignore the attributes", "--no-attributes")
+
+       # Do not generate documentation for private properties.
+       var opt_private = new OptionBool("also generate private API", "--private")
+
+       redef init do
+               super
+               option_context.add_option(opt_no_attributes, opt_private)
+       end
+
+       # Minimum visibility displayed.
+       #
+       # See `opt_private`.
+       var min_visibility: MVisibility is lazy do
+               if opt_private.value then return none_visibility
+               return protected_visibility
+       end
+end
+
+# ExtractionPhase populates the DocModel.
+class ExtractionPhase
+       super DocPhase
+
+       private var new_model: Model is noinit
+
+       # Populates the given DocModel.
+       redef fun apply do
+               doc.populate(self)
+       end
+
+       # Should we exclude this `mproject` from the documentation?
+       fun ignore_mentity(mentity: MEntity): Bool do
+               if mentity isa MModule then
+                       return mentity.is_fictive or mentity.is_test_suite
+               else if mentity isa MClass then
+                       return mentity.visibility < ctx.min_visibility
+               else if mentity isa MClassDef then
+                       return ignore_mentity(mentity.mclass)
+               else if mentity isa MProperty then
+                       return ignore_mentity(mentity.intro_mclassdef) or
+                               mentity.visibility < ctx.min_visibility or
+                               (ctx.opt_no_attributes.value and mentity isa MAttribute) or
+                               mentity isa MInnerClass
+               else if mentity isa MPropDef then
+                       return ignore_mentity(mentity.mclassdef) or
+                               ignore_mentity(mentity.mproperty)
+               end
+               return false
+       end
+end
+
+# TODO Should I rebuild a new Model from filtered data?
+redef class DocModel
+
+       # MProjects that will be documented.
+       var mprojects = new HashSet[MProject]
+
+       # MGroups that will be documented.
+       var mgroups = new HashSet[MGroup]
+
+       # MModules that will be documented.
+       var mmodules = new HashSet[MModule]
+
+       # MClasses that will be documented.
+       var mclasses = new HashSet[MClass]
+
+       # MClassDefs that will be documented.
+       var mclassdefs = new HashSet[MClassDef]
+
+       # MProperties that will be documented.
+       var mproperties = new HashSet[MProperty]
+
+       # MPropdefs that will be documented.
+       var mpropdefs = new HashSet[MPropDef]
+
+       # Populate `self` from internal `model`.
+       fun populate(v: ExtractionPhase) do
+               populate_mprojects(v)
+               populate_mclasses(v)
+               populate_mproperties(v)
+       end
+
+       # Populates the `mprojects` set.
+       private fun populate_mprojects(v: ExtractionPhase) do
+               for mproject in model.mprojects do
+                       if v.ignore_mentity(mproject) then continue
+                       self.mprojects.add mproject
+                       for mgroup in mproject.mgroups do
+                               if v.ignore_mentity(mgroup) then continue
+                               self.mgroups.add mgroup
+                               for mmodule in mgroup.mmodules do
+                                       if v.ignore_mentity(mmodule) then continue
+                                       self.mmodules.add mmodule
+                               end
+                       end
+               end
+       end
+
+       # Populates the `mclasses` set.
+       private fun populate_mclasses(v: ExtractionPhase) do
+               for mclass in model.mclasses do
+                       if v.ignore_mentity(mclass) then continue
+                       self.mclasses.add mclass
+                       for mclassdef in mclass.mclassdefs do
+                               if v.ignore_mentity(mclassdef) then continue
+                               self.mclassdefs.add mclassdef
+                       end
+               end
+       end
+
+       # Populates the `mproperties` set.
+       private fun populate_mproperties(v: ExtractionPhase) do
+               for mproperty in model.mproperties do
+                       if v.ignore_mentity(mproperty) then continue
+                       self.mproperties.add mproperty
+                       for mpropdef in mproperty.mpropdefs do
+                               if v.ignore_mentity(mpropdef) then continue
+                               self.mpropdefs.add mpropdef
+                       end
+               end
+       end
+end
diff --git a/src/doc/doc_phases/doc_graphs.nit b/src/doc/doc_phases/doc_graphs.nit
new file mode 100644 (file)
index 0000000..6013088
--- /dev/null
@@ -0,0 +1,123 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Adds importation and class hierarchy graphs.
+module doc_graphs
+
+import doc_poset
+
+redef class ToolContext
+
+       # Do not generate `graphviz` diagrams.
+       var opt_nodot = new OptionBool("do not generate graphes with graphviz", "--no-dot")
+
+       redef init do
+               super
+               option_context.add_option(opt_nodot)
+       end
+end
+
+# This phase insert importation and inheritance graphs into pages.
+class GraphPhase
+       super DocPhase
+
+       redef fun apply do
+               if ctx.opt_nodot.value then return
+               for page in doc.pages do
+                       var article = page.build_graph(self, doc)
+                       if article == null then continue
+                       # FIXME avoid diff
+                       # page.root.add article
+                       page.root.children[1].children.insert(article, 0)
+               end
+       end
+end
+
+redef class DocPage
+       # Build dot graph articles from `mmodules` list.
+       #
+       # Since only `MEntity pages` contain a graph, this method returns null in all
+       # other cases.
+       private fun build_graph(v: GraphPhase, doc: DocModel): nullable GraphArticle do return null
+end
+
+# TODO graph generation can be factorized in POSet.
+
+redef class MModulePage
+       redef fun build_graph(v, doc) do
+               var op = new FlatBuffer
+               var name = "dep_module_{mentity.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.mentity then
+                               op.append("\"{mmodule.name.escape_to_dot}\"[shape=box,margin=0.03];\n")
+                       else
+                               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.escape_to_dot}\"->\"{omodule.name.escape_to_dot}\";\n")
+                       end
+               end
+               op.append("\}\n")
+               return new GraphArticle(name, op)
+       end
+end
+
+redef class MClassPage
+       redef fun build_graph(v, doc) do
+               var op = new FlatBuffer
+               var name = "dep_class_{mentity.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]
+               doc.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 == self.mentity then
+                               op.append("\"{c.name.escape_to_dot}\"[shape=box,margin=0.03];\n")
+                       else
+                               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.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 new GraphArticle(name, op)
+       end
+end
+
+# An article that display an importation or inheritance graph.
+#
+# The graph is stored in dot format.
+# The final output is delayed untill rendering.
+class GraphArticle
+       super DocComposite
+
+       # Graph ID (used for outputing file with names).
+       var id: String
+
+       # Dot script of the graph.
+       var dot: Text
+end
diff --git a/src/doc/doc_phases/doc_hierarchies.nit b/src/doc/doc_phases/doc_hierarchies.nit
new file mode 100644 (file)
index 0000000..ba98ee4
--- /dev/null
@@ -0,0 +1,92 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Computes importation and class hierarchy lists.
+module doc_hierarchies
+
+import doc_structure
+import doc_poset
+
+# Insert inheritance / importation lists in the page.
+class InheritanceListsPhase
+       super DocPhase
+
+       # Used to sort list by name.
+       var name_sorter = new MEntityNameSorter
+
+       redef fun apply do
+               for page in doc.pages do
+                       if page isa MEntityPage then page.build_inh_list(self, doc)
+               end
+       end
+end
+
+redef class MEntityPage
+
+       # Build importation / inheritance list for this page.
+       fun build_inh_list(v: InheritanceListsPhase, doc: DocModel) do end
+end
+
+redef class MModulePage
+       redef fun build_inh_list(v, doc) do
+               var section = new ImportationListSection
+               var imports = self.imports.to_a
+               v.name_sorter.sort(imports)
+               section.children.add new HierarchyListArticle(mentity, "Imports", imports)
+               var clients = self.clients.to_a
+               v.name_sorter.sort(clients)
+               section.children.add new HierarchyListArticle(mentity, "Clients", clients)
+               root.children.insert(section, 1)
+       end
+end
+
+redef class MClassPage
+       redef fun build_inh_list(v, doc) do
+               var section = new InheritanceListSection
+               var parents = self.parents.to_a
+               v.name_sorter.sort(parents)
+               section.children.add new HierarchyListArticle(mentity, "Parents", parents)
+               var ancestors = self.ancestors.to_a
+               v.name_sorter.sort(ancestors)
+               section.children.add new HierarchyListArticle(mentity, "Ancestors", ancestors)
+               var children = self.children.to_a
+               v.name_sorter.sort(children)
+               section.children.add new HierarchyListArticle(mentity, "Children", children)
+               var descendants = self.descendants.to_a
+               v.name_sorter.sort(descendants)
+               section.children.add new HierarchyListArticle(mentity, "Descendants", descendants)
+               root.children.insert(section, 1)
+       end
+end
+
+# FIXME diff hack
+class ImportationListSection
+       super DocSection
+end
+
+# FIXME diff hack
+class InheritanceListSection
+       super DocSection
+end
+
+# Dislay a hierarchical list of mentities.
+class HierarchyListArticle
+       super MEntityArticle
+
+       # Title displayed in the top of this list.
+       var list_title: String
+
+       # MEntities to display in this list.
+       var mentities: Array[MEntity]
+end
diff --git a/src/doc/doc_phases/doc_html.nit b/src/doc/doc_phases/doc_html.nit
new file mode 100644 (file)
index 0000000..a4bdeb0
--- /dev/null
@@ -0,0 +1,844 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Render the DocModel pages as HTML pages.
+#
+# FIXME this module is all f*cked up to maintain compatibility with
+# the original `doc_templates` and `doc_model` modules.
+# This will change in further refactorings.
+module doc_html
+
+import doc_structure
+import doc_hierarchies
+import doc_intros_redefs
+import doc_graphs
+
+redef class ToolContext
+
+       # File pattern used to link documentation to source code.
+       var opt_source = new OptionString("link for source (%f for filename, " +
+               "%l for first line, %L for last line)", "--source")
+
+       # Directory where the CSS and JS is stored.
+       var opt_sharedir = new OptionString("directory containing nitdoc assets", "--sharedir")
+
+       # Use a shareurl instead of copy shared files.
+       #
+       # This is usefull if you don't want to store the Nitdoc templates with your
+       # documentation.
+       var opt_shareurl = new OptionString("use shareurl instead of copy shared files", "--shareurl")
+
+       # Use a custom title for the homepage.
+       var opt_custom_title = new OptionString("custom title for homepage", "--custom-title")
+
+       # Display a custom brand or logo in the documentation top menu.
+       var opt_custom_brand = new OptionString("custom link to external site", "--custom-brand")
+
+       # Display a custom introduction text before the projects overview.
+       var opt_custom_intro = new OptionString("custom intro text for homepage", "--custom-overview-text")
+       # Display a custom footer on each documentation page.
+       #
+       # Generally used to display the documentation or product version.
+       var opt_custom_footer = new OptionString("custom footer text", "--custom-footer-text")
+
+       # Piwik tracker URL.
+       #
+       # If you want to monitor your visitors.
+       var opt_piwik_tracker = new OptionString("Piwik tracker URL (ex: nitlanguage.org/piwik/)", "--piwik-tracker")
+
+       # Piwik tracker site id.
+       var opt_piwik_site_id = new OptionString("Piwik site ID", "--piwik-site-id")
+
+       # These options are not currently used in Nitdoc.
+
+       # FIXME redo the plugin
+       var opt_github_upstream = new OptionString("Git branch where edited commits will be pulled into (ex: user:repo:branch)", "--github-upstream")
+       # FIXME redo the plugin
+       var opt_github_base_sha1 = new OptionString("Git sha1 of base commit used to create pull request", "--github-base-sha1")
+       # FIXME redo the plugin
+       var opt_github_gitdir = new OptionString("Git working directory used to resolve path name (ex: /home/me/myproject/)", "--github-gitdir")
+
+       redef init do
+               super
+
+               option_context.add_option(
+                       opt_source, opt_sharedir, opt_shareurl, opt_custom_title,
+                       opt_custom_footer, opt_custom_intro, opt_custom_brand,
+                       opt_github_upstream, opt_github_base_sha1, opt_github_gitdir,
+                       opt_piwik_tracker, opt_piwik_site_id)
+       end
+
+       redef fun process_options(args) do
+               super
+               var upstream = opt_github_upstream
+               var base_sha = opt_github_base_sha1
+               var git_dir = opt_github_gitdir
+               var opts = [upstream.value, base_sha.value, git_dir.value]
+               if not opts.has_only(null) and opts.has(null) then
+                       print "Error: Options {upstream.names.first}, " +
+                               "{base_sha.names.first} and {git_dir.names.first} " +
+                               "are required to enable the GitHub plugin"
+                       exit 1
+               end
+       end
+end
+
+# Render the Nitdoc as a HTML website.
+class RenderHTMLPhase
+       super DocPhase
+
+       # Used to sort sidebar elements by name.
+       var name_sorter = new MEntityNameSorter
+
+       redef fun apply do
+               init_output_dir
+               for page in doc.pages do
+                       page.render(self, doc).write_to_file("{ctx.output_dir.to_s}/{page.page_url}")
+               end
+       end
+
+       # Creates the output directory and imports assets files form `resources/`.
+       fun init_output_dir do
+               # 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 = ctx.opt_sharedir.value
+               if sharedir == null then
+                       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 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.escape_to_sh}/resources/ {output_dir.to_s.escape_to_sh}/resources/")
+               end
+
+       end
+
+       # A source link template for a given location
+       fun tpl_showsource(location: nullable Location): nullable String
+       do
+               if location == null then return null
+               var source = ctx.opt_source.value
+               if source == null then
+                       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)
+               x = source.split_with("%l")
+               source = x.join(location.line_start.to_s)
+               x = source.split_with("%L")
+               source = x.join(location.line_end.to_s)
+               source = source.simplify_path
+               return "<a target='_blank' title='Show source' href=\"{source.to_s.html_escape}\">View Source</a>"
+       end
+end
+
+redef class DocPage
+
+       # Render the page as a html template.
+       private fun render(v: RenderHTMLPhase, doc: DocModel): TplPage do
+               var shareurl = "."
+               if v.ctx.opt_shareurl.value != null then
+                       shareurl = v.ctx.opt_shareurl.value.as(not null)
+               end
+
+               # build page
+               var tpl = new TplPage
+               tpl.title = tpl_title(v, doc)
+               tpl.url = page_url
+               tpl.shareurl = shareurl
+               tpl.topmenu = tpl_topmenu(v, doc)
+               tpl.add_section tpl_content(v, doc)
+               tpl.footer = v.ctx.opt_custom_footer.value
+               tpl.body_attrs.add(new TagAttribute("data-bootstrap-share", shareurl))
+               tpl.sidebar = tpl_sidebar(v, doc)
+
+               # piwik tracking
+               var tracker_url = v.ctx.opt_piwik_tracker.value
+               var site_id = v.ctx.opt_piwik_site_id.value
+               if tracker_url != null and site_id != null then
+                       tpl.scripts.add new TplPiwikScript(tracker_url, site_id)
+               end
+               return tpl
+       end
+
+       # FIXME diff hack
+       # all properties below are roughly copied from `doc_pages`
+
+       # URL to this page.
+       fun page_url: String is abstract
+
+       # Build page sidebar if any
+       fun tpl_sidebar(v: RenderHTMLPhase, doc: DocModel): nullable TplSidebar do return null
+
+       # Build page title string
+       fun tpl_title(v: RenderHTMLPhase, doc: DocModel): String do
+               if v.ctx.opt_custom_title.value != null then
+                       return v.ctx.opt_custom_title.value.to_s
+               end
+               return "Nitdoc"
+       end
+
+       # Build top menu template
+       fun tpl_topmenu(v: RenderHTMLPhase, doc: DocModel): TplTopMenu do
+               var topmenu = new TplTopMenu(page_url)
+               var brand = v.ctx.opt_custom_brand.value
+               if brand != null then
+                       var tpl = new Template
+                       tpl.add "<span class='navbar-brand'>"
+                       tpl.add brand
+                       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
+
+       # Build page content template
+       fun tpl_content(v: RenderHTMLPhase, doc: DocModel): TplSection is abstract
+end
+
+redef class OverviewPage
+       redef fun page_url do return "index.html"
+
+       redef fun tpl_title(v, doc) do
+               if v.ctx.opt_custom_title.value != null then
+                       return v.ctx.opt_custom_title.value.to_s
+               else
+                       return "Overview"
+               end
+       end
+
+       # TODO this should be done in StructurePhase.
+       redef fun tpl_content(v, doc) do
+               # intro text
+               var section = new TplSection.with_title("overview", tpl_title(v, doc))
+               var article = new TplArticle("intro")
+               if v.ctx.opt_custom_intro.value != null then
+                       article.content = v.ctx.opt_custom_intro.value.to_s
+               end
+               section.add_child article
+               # Projects list
+               var mprojects = doc.model.mprojects.to_a
+               var sorter = new MConcernRankSorter
+               sorter.sort mprojects
+               var ssection = new TplSection.with_title("projects", "Projects")
+               for mproject in mprojects do
+                       var sarticle = mproject.tpl_article
+                       sarticle.subtitle = mproject.tpl_declaration
+                       sarticle.content = mproject.tpl_definition
+                       var mdoc = mproject.mdoc_or_fallback
+                       if mdoc != null then
+                               sarticle.content = mdoc.tpl_short_comment
+                       end
+                       ssection.add_child sarticle
+               end
+               section.add_child ssection
+               return section
+       end
+
+       redef fun tpl_sidebar(v, doc) do return new TplSidebar
+end
+
+redef class SearchPage
+       redef fun page_url do return "search.html"
+       redef fun tpl_title(v, doc) do return "Index"
+
+       # TODO this should be done in StructurePhase.
+       redef fun tpl_content(v, doc) do
+               var tpl = new TplSearchPage("search_all")
+               var section = new TplSection("search")
+               # title
+               tpl.title = "Index"
+               # modules list
+               for mmodule in modules_list(v, doc) do
+                       tpl.modules.add mmodule.tpl_link
+               end
+               # classes list
+               for mclass in classes_list(v, doc) do
+                       tpl.classes.add mclass.tpl_link
+               end
+               # properties list
+               for mproperty in mprops_list(v, doc) do
+                       var m = new Template
+                       m.add mproperty.intro.tpl_link
+                       m.add " ("
+                       m.add mproperty.intro.mclassdef.mclass.tpl_link
+                       m.add ")"
+                       tpl.props.add m
+               end
+               section.add_child tpl
+               return section
+       end
+
+       # Extract mmodule list to display (sorted by name)
+       private fun modules_list(v: RenderHTMLPhase, doc: DocModel): Array[MModule] do
+               var sorted = new Array[MModule]
+               for mmodule in doc.model.mmodule_importation_hierarchy do
+                       if mmodule.is_fictive or mmodule.is_test_suite then continue
+                       sorted.add mmodule
+               end
+               v.name_sorter.sort(sorted)
+               return sorted
+       end
+
+       # Extract mclass list to display (sorted by name)
+       private fun classes_list(v: RenderHTMLPhase, doc: DocModel): Array[MClass] do
+               var sorted = doc.mclasses.to_a
+               v.name_sorter.sort(sorted)
+               return sorted
+       end
+
+       # Extract mproperty list to display (sorted by name)
+       private fun mprops_list(v: RenderHTMLPhase, doc: DocModel): Array[MProperty] do
+               var sorted = doc.mproperties.to_a
+               v.name_sorter.sort(sorted)
+               return sorted
+       end
+end
+
+redef class MEntityPage
+       redef fun page_url do return mentity.nitdoc_url
+       redef fun tpl_title(v, doc) do return mentity.nitdoc_name
+       redef fun tpl_content(v, doc) do return root.start_rendering(v, doc, self)
+end
+
+# FIXME all clases below are roughly copied from `doc_pages` and adapted to new
+# doc phases. This is to preserve the compatibility with the current
+# `doc_templates` module.
+
+redef class MGroupPage
+       redef fun tpl_topmenu(v, doc) do
+               var topmenu = super
+               var mproject = mentity.mproject
+               if not mentity.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
+
+       redef fun tpl_sidebar(v, doc) do
+               var sidebar = new TplSidebar
+               var mclasses = new HashSet[MClass]
+               mclasses.add_all intros
+               mclasses.add_all redefs
+               if mclasses.is_empty then return sidebar
+               var list = new TplList.with_classes(["list-unstyled", "list-labeled"])
+
+               var sorted = mclasses.to_a
+               v.name_sorter.sort(sorted)
+               for mclass in sorted do
+                       list.add_li tpl_sidebar_item(mclass)
+               end
+               sidebar.boxes.add new TplSideBox.with_content("All classes", list)
+               return sidebar
+       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
+end
+
+redef class MModulePage
+       redef fun tpl_topmenu(v, doc) do
+               var topmenu = super
+               var mproject = mentity.mproject
+               topmenu.add_link new TplLink(mproject.nitdoc_url, mproject.nitdoc_name)
+               topmenu.add_link new TplLink(mentity.nitdoc_url, mentity.nitdoc_name)
+               return topmenu
+       end
+
+       # Class list to display in sidebar
+       redef fun tpl_sidebar(v, doc) do
+               # TODO filter here?
+               var sidebar = new TplSidebar
+               var mclasses = new HashSet[MClass]
+               mclasses.add_all mentity.filter_intro_mclasses(v.ctx.min_visibility)
+               mclasses.add_all mentity.filter_redef_mclasses(v.ctx.min_visibility)
+               if mclasses.is_empty then return sidebar
+               var list = new TplList.with_classes(["list-unstyled", "list-labeled"])
+
+               var sorted = mclasses.to_a
+               v.name_sorter.sort(sorted)
+               for mclass in sorted do
+                       list.add_li tpl_sidebar_item(mclass)
+               end
+               sidebar.boxes.add new TplSideBox.with_content("All classes", list)
+               return sidebar
+       end
+
+       private fun tpl_sidebar_item(def: MClass): TplListItem do
+               var classes = def.intro.tpl_css_classes.to_a
+               if def.intro_mmodule == self.mentity 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
+end
+
+redef class MClassPage
+
+       redef fun tpl_title(v, doc) do
+               return "{mentity.nitdoc_name}{mentity.tpl_signature.write_to_string}"
+       end
+
+       redef fun tpl_topmenu(v, doc) do
+               var topmenu = super
+               var mproject = mentity.intro_mmodule.mgroup.mproject
+               topmenu.add_link new TplLink("{mproject.nitdoc_url}", "{mproject.nitdoc_name}")
+               topmenu.add_link new TplLink(page_url, mentity.nitdoc_name)
+               return topmenu
+       end
+
+       redef fun tpl_sidebar(v, doc) do
+               var sidebar = new TplSidebar
+               var by_kind = new PropertiesByKind.with_elements(mclass_inherited_mprops(v, doc))
+               var summary = new TplList.with_classes(["list-unstyled"])
+
+               by_kind.sort_groups(v.name_sorter)
+               for g in by_kind.groups do tpl_sidebar_list(g, summary)
+               sidebar.boxes.add new TplSideBox.with_content("All properties", summary)
+               return sidebar
+       end
+
+       private fun tpl_sidebar_list(mprops: PropertyGroup[MProperty], summary: TplList) do
+               if mprops.is_empty then return
+               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)
+               end
+               entry.append list
+               summary.elts.add entry
+       end
+
+       private fun tpl_sidebar_item(mprop: MProperty): TplListItem do
+               var classes = mprop.intro.tpl_css_classes.to_a
+               if not mprop_is_local(mprop) then
+                       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.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
+                       return new TplListItem.with_content(item)
+               end
+               if mpropdefs.has(mprop.intro) then
+                       classes.add "intro"
+               else
+                       classes.add "redef"
+               end
+               var lnk = new Template
+               lnk.add new TplLabel.with_classes(classes)
+               lnk.add mprop.tpl_anchor
+               return new TplListItem.with_content(lnk)
+       end
+
+       private fun mclass_inherited_mprops(v: RenderHTMLPhase, doc: DocModel): Set[MProperty] do
+               var res = new HashSet[MProperty]
+               var local = mentity.local_mproperties(v.ctx.min_visibility)
+               for mprop in mentity.inherited_mproperties(doc.mainmodule, v.ctx.min_visibility) do
+                       if local.has(mprop) then continue
+                       #if mprop isa MMethod and mprop.is_init then continue
+                       if mprop.intro.mclassdef.mclass.name == "Object" and
+                               (mprop.visibility == protected_visibility or
+                               mprop.intro.mclassdef.mmodule.name != "kernel") then continue
+                       res.add mprop
+               end
+               res.add_all local
+               return res
+       end
+
+       private fun mprop_is_local(mprop: MProperty): Bool do
+               for mpropdef in mprop.mpropdefs do
+                       if self.mpropdefs.has(mpropdef) then return true
+               end
+               return false
+       end
+end
+
+redef class MPropertyPage
+       redef fun tpl_topmenu(v, doc) do
+               var topmenu = super
+               var mmodule = mentity.intro_mclassdef.mmodule
+               var mproject = mmodule.mgroup.mproject
+               var mclass = mentity.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, mentity.nitdoc_name)
+               return topmenu
+       end
+
+       redef fun tpl_title(v, doc) do
+               return "{mentity.nitdoc_name}{mentity.tpl_signature.write_to_string}"
+       end
+
+       redef fun tpl_sidebar(v, doc) do return new TplSidebar
+end
+
+redef class DocComposite
+       # Render this DocComposite as HTML.
+       #
+       # FIXME needed to maintain TplSection compatibility.
+       fun render(v: RenderHTMLPhase, doc: DocModel, page: MEntityPage, parent: TplSectionElt) is abstract
+end
+
+redef class DocRoot
+
+       # Start the rendering from root.
+       #
+       # FIXME needed to maintain TplSection compatibility.
+       fun start_rendering(v: RenderHTMLPhase, doc: DocModel, page: MEntityPage): TplSection do
+               var section = new TplSection("top")
+               var mentity = page.mentity
+               section.title = mentity.nitdoc_name
+               section.subtitle = mentity.tpl_declaration
+               # FIXME ugly hack to avoid diff
+               if mentity isa MGroup and mentity.is_root then
+                       section.title = mentity.mproject.nitdoc_name
+                       section.subtitle = mentity.mproject.tpl_declaration
+               else if mentity isa MClass then
+                       section.title = "{mentity.nitdoc_name}{mentity.tpl_signature.write_to_string}"
+               else if mentity isa MProperty then
+                       section.title = "{mentity.nitdoc_name}{mentity.intro.tpl_signature.write_to_string}"
+                       section.subtitle = mentity.tpl_namespace
+                       section.summary_title = mentity.nitdoc_name
+               end
+               render(v, doc, page, section)
+               return section
+       end
+
+       redef fun render(v, doc, page, parent) do
+               for child in children do
+                       child.render(v, doc, page, parent)
+               end
+       end
+end
+
+redef class ConcernSection
+       redef fun render(v, doc, page, parent) do
+               var section = new TplSection(mentity.nitdoc_id)
+               var mentity = self.mentity
+               # FIXME hideous hacks to avoid diff
+               if page.mentity isa MModule and mentity isa MModule then
+                       render_concern_mmodule(page, section, mentity)
+               else if page.mentity isa MClass and mentity isa MModule then
+                       render_concern_other(page, section, mentity)
+               else if page.mentity isa MProperty and mentity isa MModule then
+                       render_concern_other(page, section, mentity)
+               end
+               for child in children do
+                       child.render(v, doc, page, section)
+               end
+               parent.add_child section
+       end
+
+       private fun render_concern_mmodule(page: MEntityPage, section: TplSection, mmodule: MModule) do
+               var title = new Template
+               if mmodule == page.mentity then
+                       title.add "in "
+                       section.summary_title = "in {mmodule.nitdoc_name}"
+               else
+                       title.add "from "
+                       section.summary_title = "from {mmodule.nitdoc_name}"
+               end
+               title.add mmodule.tpl_namespace
+               section.title = title
+       end
+
+       private fun render_concern_other(page: MEntityPage, section: TplSection, mmodule: MModule) do
+               var title = new Template
+               title.add "in "
+               title.add mmodule.tpl_namespace
+               section.title = title
+               section.summary_title = "in {mmodule.nitdoc_name}"
+       end
+end
+
+redef class IntroArticle
+       redef fun render(v, doc, page, parent) do
+               var article = new TplArticle("intro")
+               var mentity = self.mentity
+               if mentity isa MModule then
+                       article.source_link = v.tpl_showsource(mentity.location)
+               else if mentity isa MClassDef then
+                       article.source_link = v.tpl_showsource(mentity.location)
+               else if mentity isa MPropDef then
+                       article.source_link = v.tpl_showsource(mentity.location)
+               end
+               # article.subtitle = mentity.tpl_declaration
+               # FIXME diff hack
+               if mentity isa MProperty then
+                       # intro title
+                       var ns = mentity.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"
+                       var intro = mentity.intro.tpl_article
+                       intro.source_link = v.tpl_showsource(mentity.intro.location)
+                       section.add_child intro
+                       parent.add_child section
+               else
+                       article.content = mentity.tpl_definition
+                       parent.add_child article
+               end
+       end
+end
+
+redef class ConcernsArticle
+       redef fun render(v, doc, page, parent) do
+               # FIXME diff hack
+               var title = "concerns"
+               if page.mentity isa MProperty then title = "Concerns"
+               parent.add_child new TplArticle.
+                       with_content(title, "Concerns", concerns.to_tpl)
+       end
+end
+
+redef class DefinitionArticle
+       redef fun render(v, doc, page, parent) do
+               var article: TplArticle
+               var mentity = self.mentity
+               # FIXME hideous hacks...
+               if mentity isa MModule then
+                       article = mentity.tpl_article
+                       article.subtitle = mentity.tpl_declaration
+                       article.content = mentity.tpl_definition
+               else if mentity isa MClass then
+                       article = make_mclass_article(v, page)
+               else if mentity isa MClassDef then
+                       article = make_mclassdef_article(v, page)
+                       article.source_link = v.tpl_showsource(mentity.location)
+               else if mentity isa MPropDef and page.mentity isa MClass then
+                       article = make_mpropdef_article(v, doc, page)
+               else
+                       article = mentity.tpl_article
+                       article.subtitle = mentity.tpl_declaration
+                       if mentity isa MPropDef then
+                               article.source_link = v.tpl_showsource(mentity.location)
+                       end
+                       if not mentity isa MVirtualTypeProp then
+                               # article.content = mentity.tpl_comment
+                       end
+               end
+               for child in children do
+                       child.render(v, doc, page, article)
+               end
+               parent.add_child article
+       end
+
+       # FIXME avoid diff while preserving TplArticle compatibility.
+
+       private fun make_mclass_article(v: RenderHTMLPhase, page: MEntityPage): TplArticle do
+               var article = mentity.tpl_article
+               article.subtitle = mentity.tpl_namespace
+               article.content = null
+               return article
+       end
+
+       private fun make_mclassdef_article(v: RenderHTMLPhase, page: MEntityPage): TplArticle do
+               var mclassdef = mentity.as(MClassDef)
+               var article = mentity.tpl_article
+               if mclassdef.is_intro and mclassdef.mmodule != page.mentity then
+                       article = mentity.tpl_short_article
+               end
+               var title = new Template
+               title.add "in "
+               title.add mclassdef.mmodule.tpl_namespace
+               article.subtitle = title
+               return article
+       end
+
+       private fun make_mpropdef_article(v: RenderHTMLPhase, doc: DocModel, page: MEntityPage): TplArticle
+       do
+               var mpropdef = mentity.as(MPropDef)
+               var mprop = mpropdef.mproperty
+               var article = new TplArticle(mprop.nitdoc_id)
+               var title = new Template
+               title.add mprop.tpl_icon
+               title.add "<span id='{mpropdef.nitdoc_id}'></span>"
+               if 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.nitdoc_name,
+                                       "Go to introduction")
+                       title.add "redef "
+                       title.add lnk
+               end
+               article.title = title
+               article.title_classes.add "signature"
+               article.summary_title = "{mprop.nitdoc_name}"
+               article.subtitle = mpropdef.tpl_namespace
+               if mpropdef.mdoc_or_fallback != null then
+                       article.content = mpropdef.mdoc_or_fallback.tpl_comment
+               end
+               # TODO move in its own phase? let's see after doc_template refactoring.
+               # Add linearization
+               var all_defs = new HashSet[MPropDef]
+               for local_def in local_defs(page.as(MClassPage), mprop) do
+                       all_defs.add local_def
+                       var smpropdef = local_def
+                       while not smpropdef.is_intro do
+                               smpropdef = smpropdef.lookup_next_definition(
+                                       doc.mainmodule, smpropdef.mclassdef.bound_mtype)
+                               all_defs.add smpropdef
+                       end
+               end
+               var lin = all_defs.to_a
+               doc.mainmodule.linearize_mpropdefs(lin)
+               if lin.length > 1 then
+                       var lin_article = new TplArticle("{mpropdef.nitdoc_id}.lin")
+                       lin_article.title = "Inheritance"
+                       var lst = new TplList.with_classes(["list-unstyled", "list-labeled"])
+                       for smpropdef in lin do
+                               lst.add_li smpropdef.tpl_inheritance_item
+                       end
+                       lin_article.content = lst
+                       article.add_child lin_article
+               end
+               return article
+       end
+
+       # Filter `page.mpropdefs` for this `mpropertie`.
+       #
+       # FIXME compatability with current templates.
+       private fun local_defs(page: MClassPage, mproperty: MProperty): HashSet[MPropDef] do
+               var mpropdefs = new HashSet[MPropDef]
+               for mpropdef in page.mpropdefs do
+                       if mpropdef.mproperty == mproperty then
+                               mpropdefs.add mpropdef
+                       end
+               end
+               return mpropdefs
+       end
+end
+
+redef class IntrosRedefsListArticle
+       redef fun render(v, doc, page, parent) do
+               if mentities.is_empty then return
+               var title = list_title
+               # FIXME diff hack
+               var id = "intros"
+               if title == "Redefines" then id = "redefs"
+               var article = new TplArticle.with_title("{mentity.nitdoc_id}.{id}", title)
+               var list = new TplList.with_classes(["list-unstyled", "list-labeled"])
+               for mentity in mentities do
+                       list.add_li mentity.tpl_list_item
+               end
+               article.content = list
+               parent.add_child article
+       end
+end
+
+# FIXME compatibility with doc_templates.
+redef class ImportationListSection
+       redef fun render(v, doc, page, parent) do
+               var section = new TplSection.with_title("dependencies", "Dependencies")
+               for child in children do
+                       child.render(v, doc, page, section)
+               end
+               parent.add_child section
+       end
+end
+
+# FIXME compatibility with doc_templates.
+redef class InheritanceListSection
+       redef fun render(v, doc, page, parent) do
+               var section = new TplSection.with_title("inheritance", "Inheritance")
+               for child in children do
+                       child.render(v, doc, page, section)
+               end
+               parent.add_child section
+       end
+end
+
+# FIXME compatibility with doc_templates.
+redef class HierarchyListArticle
+       redef fun render(v, doc, page, parent) do
+               if mentities.is_empty then return
+               var title = list_title
+               var id = list_title.to_lower
+               var article = new TplArticle.with_title(id, title)
+               var list = new TplList.with_classes(["list-unstyled", "list-definition"])
+               for mentity in mentities do
+                       list.elts.add mentity.tpl_list_item
+               end
+               article.content = list
+               parent.add_child article
+       end
+end
+
+redef class GraphArticle
+       redef fun render(v, doc, page, parent) do
+               var output_dir = v.ctx.output_dir
+               var path = output_dir / id
+               var path_sh = path.escape_to_sh
+               var file = new OFStream.open("{path}.dot")
+               file.write(dot)
+               file.close
+               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("graph")
+               var alt = ""
+               # FIXME diff hack
+               # if title != null then
+                       # article.title = title
+                       # alt = "alt='{title.html_escape}'"
+               # end
+               article.css_classes.add "text-center"
+               var content = new Template
+               var name_html = id.html_escape
+               content.add "<img src='{name_html}.png' usemap='#{name_html}' style='margin:auto' {alt}/>"
+               content.add map
+               article.content = content
+               parent.add_child article
+       end
+end
diff --git a/src/doc/doc_phases/doc_indexing.nit b/src/doc/doc_phases/doc_indexing.nit
new file mode 100644 (file)
index 0000000..9e9ef3c
--- /dev/null
@@ -0,0 +1,106 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Manage indexing of Nit model for Nitdoc QuickSearch.
+module doc_indexing
+
+import doc_extract
+private import json::static
+
+# Generate the index for then Nitdoc QuickSearch field.
+#
+# Create a JSON object containing links to:
+#  * modules
+#  * mclasses
+#  * mpropdefs
+# All entities are grouped by name to make the research easier.
+#
+# TODO Add a way to change the output and use it from Vim or whatever.
+class IndexingPhase
+       super DocPhase
+
+       redef fun apply do
+               for mmodule in doc.mmodules do
+                       add_result_for(mmodule.name, mmodule.full_name, mmodule.nitdoc_url)
+               end
+               for mclass in doc.mclasses do
+                       add_result_for(mclass.name, mclass.full_name, mclass.nitdoc_url)
+               end
+               for mproperty in doc.mproperties do
+                       for mpropdef in mproperty.mpropdefs do
+                               if not doc.mpropdefs.has(mpropdef) then continue
+                               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
+               end
+               # FIXME hack, generation should be done by the render phase
+               # create destination dir if it's necessary
+               var output_dir = ctx.output_dir
+               if not output_dir.file_exists then output_dir.mkdir
+
+               render.write_to_file("{ctx.output_dir.to_s}/quicksearch-list.js")
+       end
+
+       private var table = new QuickSearchTable
+
+       private fun add_result_for(query: String, txt: String, url: String) do
+               table[query].add new QuickSearchResult(txt, url)
+       end
+
+       # Render the index content.
+       fun render: Template do
+               var tpl = new Template
+               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
diff --git a/src/doc/doc_phases/doc_intros_redefs.nit b/src/doc/doc_phases/doc_intros_redefs.nit
new file mode 100644 (file)
index 0000000..85c2c80
--- /dev/null
@@ -0,0 +1,91 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Generates lists about intros/redefs in MEntity.
+#
+# Actually, this works only for MModules and MclassDefs.
+module doc_intros_redefs
+
+import doc_structure
+
+# Computes intro / redef mentity list for each DefinitionArticle.
+class IntroRedefListPhase
+       super DocPhase
+
+       redef fun apply do
+               for page in doc.pages do
+                       if not page isa MEntityPage then continue
+                       page.root.build_intro_redef_list(self, doc, page)
+               end
+       end
+end
+
+redef class DocComposite
+
+       # Computes intro / redef lists for this page.
+       #
+       # See `IntroRedefListPhase`.
+       fun build_intro_redef_list(v: IntroRedefListPhase, doc: DocModel, page: MEntityPage) do
+               for child in children do child.build_intro_redef_list(v, doc, page)
+       end
+end
+
+redef class DefinitionArticle
+       redef fun build_intro_redef_list(v, doc, page) do
+               var mentity = self.mentity
+               if mentity isa MModule then
+                       build_mmodule_list(v, doc, mentity)
+               else if mentity isa MClassDef and mentity.mmodule == page.mentity then
+                       build_mclassdef_list(v, doc, mentity)
+               end
+               super
+       end
+
+       # TODO this should move to MEntity?
+       private fun build_mmodule_list(v: IntroRedefListPhase, doc: DocModel, mmodule: MModule) do
+               var intros = mmodule.intro_mclassdefs(v.ctx.min_visibility).to_a
+               doc.mainmodule.linearize_mclassdefs(intros)
+               children.add new IntrosRedefsListArticle(mentity, "Introduces", intros)
+               var redefs = mmodule.redef_mclassdefs(v.ctx.min_visibility).to_a
+               doc.mainmodule.linearize_mclassdefs(redefs)
+               children.add new IntrosRedefsListArticle(mentity, "Redefines", redefs)
+       end
+
+       # TODO this should move to MEntity?
+       private fun build_mclassdef_list(v: IntroRedefListPhase, doc: DocModel, mclassdef: MClassDef) do
+               var intros = mclassdef.collect_intro_mpropdefs(v.ctx.min_visibility).to_a
+               # FIXME avoid diff changes
+               # v.ctx.mainmodule.linearize_mpropdefs(intros)
+               children.add new IntrosRedefsListArticle(mentity, "Introduces", intros)
+               var redefs = mclassdef.collect_redef_mpropdefs(v.ctx.min_visibility).to_a
+               # FIXME avoid diff changes
+               # v.ctx.mainmodule.linearize_mpropdefs(redefs)
+               children.add new IntrosRedefsListArticle(mentity, "Redefines", redefs)
+       end
+
+end
+
+# An article that displays a list of introduced / refined mentities.
+#
+# FIXME diff hack
+# This can merged with InheritanceListArticle in a more generic class.
+class IntrosRedefsListArticle
+       super MEntityArticle
+
+       # Title displayed as header of the list.
+       var list_title: String
+
+       # Intro mentities to list.
+       var mentities: Array[MEntity]
+end
diff --git a/src/doc/doc_phases/doc_pages.nit b/src/doc/doc_phases/doc_pages.nit
new file mode 100644 (file)
index 0000000..2594d4b
--- /dev/null
@@ -0,0 +1,90 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Create DocPage instances for each documentated Mentity.
+module doc_pages
+
+import doc_extract
+
+# ExtractionPhase populates the DocModel with DocPage.
+class MakePagePhase
+       super DocPhase
+
+       # Instanciates documentation pages for the given DocModel.
+       redef fun apply do
+               doc.pages.add new OverviewPage("Overview")
+               doc.pages.add new SearchPage("Index")
+               for mgroup in doc.mgroups do
+                       doc.pages.add new MGroupPage(mgroup.nitdoc_id, mgroup)
+               end
+               for mmodule in doc.mmodules do
+                       doc.pages.add new MModulePage(mmodule.nitdoc_id, mmodule)
+               end
+               for mclass in doc.mclasses do
+                       doc.pages.add new MClassPage(mclass.nitdoc_id, mclass)
+               end
+               for mproperty in doc.mproperties do
+                       doc.pages.add new MPropertyPage(mproperty.nitdoc_id, mproperty)
+               end
+       end
+end
+
+# The Nitdoc overview page.
+class OverviewPage
+       super DocPage
+end
+
+# The Nidoc full index page.
+class SearchPage
+       super DocPage
+end
+
+# A DocPage documenting a MEntity.
+class MEntityPage
+       super DocPage
+
+       # Type of MEntity documented by this page.
+       type MENTITY: MEntity
+
+       # MEntity documented by this page.
+       var mentity: MENTITY
+end
+
+# A documentation page about a MGroup.
+class MGroupPage
+       super MEntityPage
+
+       redef type MENTITY: MGroup
+end
+
+# A documentation page about a MModule.
+class MModulePage
+       super MEntityPage
+
+       redef type MENTITY: MModule
+end
+
+# A documentation page about a MClass.
+class MClassPage
+       super MEntityPage
+
+       redef type MENTITY: MClass
+end
+
+# A documentation page about a MProperty.
+class MPropertyPage
+       super MEntityPage
+
+       redef type MENTITY: MProperty
+end
diff --git a/src/doc/doc_phases/doc_phases.nit b/src/doc/doc_phases/doc_phases.nit
new file mode 100644 (file)
index 0000000..fefd1ac
--- /dev/null
@@ -0,0 +1,21 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Phases represent the *steps* of the NitDoc generation process.
+#
+# See `DocPhase`.
+module doc_phases
+
+import doc_html
+import doc_indexing
diff --git a/src/doc/doc_phases/doc_poset.nit b/src/doc/doc_phases/doc_poset.nit
new file mode 100644 (file)
index 0000000..fd38e0f
--- /dev/null
@@ -0,0 +1,178 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Importation and inheritance POSet for pages.
+module doc_poset
+
+import doc_pages
+
+# This phase computes importation and inheritance POSet for pages.
+class POSetPhase
+       super DocPhase
+
+       # Populates the given DocModel.
+       redef fun apply do
+               for page in doc.pages do
+                       if page isa MEntityPage then page.build_poset(self, doc)
+               end
+       end
+end
+
+redef class MEntityPage
+
+       # The poset associated with this page.
+       #
+       # FIXME should be defined in subclasses
+       # as the POSet can contains other types than `SELF`
+       var poset = new POSet[MENTITY]
+
+       # Build the POSet for this page.
+       private fun build_poset(v: POSetPhase, doc: DocModel) do end
+end
+
+redef class MModulePage
+
+       # Imported modules that should appear in the documentation.
+       var imports = new HashSet[MModule]
+
+       # Clients modules that shjould appear in the documentation.
+       var clients = new HashSet[MModule]
+
+       redef fun build_poset(v, doc) do
+               # collect importation
+               for dep in mentity.in_importation.greaters do
+                       if dep == mentity then continue
+                       if not doc.mmodules.has(dep) then continue
+                       imports.add dep
+               end
+               # FIXME avoid diff
+               #if imports.length > 10 then
+               if mentity.in_importation.greaters.length > 10 then
+                       imports.clear
+                       for dep in mentity.in_importation.direct_greaters do
+                               if dep == mentity then continue
+                               if not doc.mmodules.has(dep) then continue
+                               imports.add dep
+                       end
+               end
+               # collect clients
+               for dep in mentity.in_importation.smallers do
+                       if dep == mentity then continue
+                       if not doc.mmodules.has(dep) then continue
+                       clients.add dep
+               end
+               if clients.length > 10 then
+                       clients.clear
+                       for dep in mentity.in_importation.direct_smallers do
+                               if dep == mentity then continue
+                               if not doc.mmodules.has(dep) then continue
+                               clients.add dep
+                       end
+               end
+               # make poset
+               var mmodules = new HashSet[MModule]
+               mmodules.add_all mentity.nested_mmodules
+               mmodules.add_all imports
+               if clients.length < 10 then mmodules.add_all clients
+               mmodules.add mentity
+               build_importation_poset(doc, mmodules)
+       end
+
+       # Build the POSet of importation from a list of `mmodules`.
+       private fun build_importation_poset(doc: DocModel, mmodules: Set[MModule]): POSet[MModule] do
+               for mmodule in mmodules do
+                       if not doc.mmodules.has(mmodule) then continue
+                       poset.add_node mmodule
+                       for omodule in mmodules do
+                               if not doc.mmodules.has(omodule) then continue
+                               poset.add_node mmodule
+                               if mmodule.in_importation < omodule then
+                                       poset.add_edge(mmodule, omodule)
+                               end
+                       end
+               end
+               return poset
+       end
+end
+
+redef class MClassPage
+
+       # Direct parents classes to document.
+       var parents = new HashSet[MClass]
+
+       # Transitive ancestors classes to document.
+       #
+       # Does not contain the direct ancestors.
+       # See `parents` for that.
+       var ancestors = new HashSet[MClass]
+
+       # Direct children classes to document.
+       var children = new HashSet[MClass]
+
+       # All descendants classes to document.
+       #
+       # Does not contain the direct children.
+       # See `children` for that.
+       var descendants = new HashSet[MClass]
+
+       redef fun build_poset(v, doc) do
+               poset.add_node mentity
+
+               var h = mentity.in_hierarchy(doc.mainmodule)
+               # parents
+               for mclass in h.direct_greaters do
+                       if doc.mclasses.has(mclass) then parents.add mclass
+               end
+               # ancestors
+               for mclass in h.greaters do
+                       if mclass == mentity then continue
+                       if not doc.mclasses.has(mclass) then continue
+                       if parents.has(mclass) then continue
+                       ancestors.add mclass
+               end
+               # children
+               for mclass in h.direct_smallers do
+                       if doc.mclasses.has(mclass) then children.add mclass
+               end
+               # descendants
+               for mclass in h.smallers do
+                       if mclass == mentity then continue
+                       if not doc.mclasses.has(mclass) then continue
+                       if children.has(mclass) then continue
+                       descendants.add mclass
+               end
+               # poset
+               var mclasses = new HashSet[MClass]
+               mclasses.add_all ancestors
+               mclasses.add_all parents
+               mclasses.add_all children
+               mclasses.add_all descendants
+               mclasses.add mentity
+               build_inheritance_poset(v, doc, mclasses)
+       end
+
+       private fun build_inheritance_poset(v: POSetPhase, doc: DocModel, mclasses: Set[MClass]): POSet[MClass] do
+               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(doc.mainmodule) < oclass then
+                                       poset.add_edge(mclass, oclass)
+                               end
+                       end
+               end
+               return poset
+       end
+end
diff --git a/src/doc/doc_phases/doc_structure.nit b/src/doc/doc_phases/doc_structure.nit
new file mode 100644 (file)
index 0000000..6fc7468
--- /dev/null
@@ -0,0 +1,280 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Composes the DocComposite tree of a DocPage and organizes its content.
+module doc_structure
+
+import doc_concerns
+
+# StructurePhase populates the DocPage content with section and article.
+#
+# This phase only applies structure.
+# The content of the structure is choosen by the rendering phases.
+class StructurePhase
+       super DocPhase
+
+       # Used to sort ConcernsTree by rank.
+       private var concerns_sorter = new MConcernRankSorter
+
+       # Used to sort ConcernsTree by name.
+       private var name_sorter = new MEntityNameSorter
+
+       # Populates the given DocModel.
+       redef fun apply do
+               for page in doc.pages do
+                       if page isa MEntityPage then page.apply_structure(self, doc)
+               end
+       end
+
+       # TODO index and search page should also be structured here
+end
+
+redef class MEntityPage
+
+       # Populates `self` with structure elements like DocComposite ones.
+       #
+       # See `StructurePhase`.
+       fun apply_structure(v: StructurePhase, doc: DocModel) do end
+end
+
+redef class MGroupPage
+       redef fun apply_structure(v, doc) do
+               if mentity.is_root then
+                       root.add new IntroArticle(mentity.mproject)
+               else
+                       root.add new IntroArticle(mentity)
+               end
+               var concerns = self.concerns
+               if concerns == null or concerns.is_empty then return
+               # FIXME avoid diff
+               mentity.mproject.booster_rank = -1000
+               mentity.booster_rank = -1000
+               concerns.sort_with(v.concerns_sorter)
+               mentity.mproject.booster_rank = 0
+               mentity.booster_rank = 0
+               root.add new ConcernsArticle(mentity, concerns)
+               for mentity in concerns do
+                       if mentity isa MModule then
+                               root.add new DefinitionArticle(mentity)
+                       else
+                               root.add new ConcernSection(mentity)
+                       end
+               end
+       end
+end
+
+redef class MModulePage
+       redef fun apply_structure(v, doc) do
+               root.add new IntroArticle(mentity)
+               var concerns = self.concerns
+               if concerns == null or concerns.is_empty then return
+               # FIXME avoid diff
+               mentity.mgroup.mproject.booster_rank = -1000
+               mentity.mgroup.booster_rank = -1000
+               mentity.booster_rank = -1000
+               concerns.sort_with(v.concerns_sorter)
+               mentity.mgroup.mproject.booster_rank = 0
+               mentity.mgroup.booster_rank = 0
+               mentity.booster_rank = 0
+               root.add new ConcernsArticle(mentity, concerns)
+               # reference list
+               for mentity in concerns do
+                       var section = new ConcernSection(mentity)
+                       if mentity isa MModule then
+                               var mclasses = mclasses_for_mmodule(mentity).to_a
+                               v.name_sorter.sort(mclasses)
+                               for mclass in mclasses do
+                                       var article = new DefinitionArticle(mclass)
+                                       var mclassdefs = mclassdefs_for(mclass).to_a
+                                       if not mclassdefs.has(mclass.intro) then
+                                               article.add(new DefinitionArticle(mclass.intro))
+                                       end
+                                       doc.mainmodule.linearize_mclassdefs(mclassdefs)
+                                       for mclassdef in mclassdefs do
+                                               article.add(new DefinitionArticle(mclassdef))
+                                       end
+                                       section.add article
+                               end
+                       end
+                       root.add section
+               end
+       end
+
+       # Filters `self.mclassses` by intro `mmodule`.
+       private fun mclasses_for_mmodule(mmodule: MModule): Set[MClass] do
+               var mclasses = new HashSet[MClass]
+               for mclass in self.mclasses do
+                       if mclass.intro.mmodule == mmodule then
+                               mclasses.add mclass
+                       end
+               end
+               return mclasses
+       end
+
+       # Filters `self.mclassdefs` by `mclass`.
+       private fun mclassdefs_for(mclass: MClass): Set[MClassDef] do
+               var mclassdefs = new HashSet[MClassDef]
+               for mclassdef in self.mclassdefs do
+                       if mclassdef.mclass == mclass then
+                               mclassdefs.add mclassdef
+                       end
+               end
+               return mclassdefs
+       end
+end
+
+redef class MClassPage
+       redef fun apply_structure(v, doc) do
+               root.add new IntroArticle(mentity)
+               var concerns = self.concerns
+               if concerns == null or concerns.is_empty then return
+               # FIXME diff hack
+               mentity.intro_mmodule.mgroup.mproject.booster_rank = -1000
+               mentity.intro_mmodule.mgroup.booster_rank = -1000
+               mentity.intro_mmodule.booster_rank = -1000
+               concerns.sort_with(v.concerns_sorter)
+               mentity.intro_mmodule.mgroup.mproject.booster_rank = 0
+               mentity.intro_mmodule.mgroup.booster_rank = 0
+               mentity.intro_mmodule.booster_rank = 0
+               root.add new ConcernsArticle(mentity, concerns)
+               for mentity in concerns do
+                       var section = new ConcernSection(mentity)
+                       if mentity isa MModule then
+                               var mprops = mproperties_for(mentity)
+                               var by_kind = new PropertiesByKind.with_elements(mprops)
+                               for group in by_kind.groups do
+                                       v.name_sorter.sort(group)
+                                       for mprop in group do
+                                               for mpropdef in mpropdefs_for(mprop, mentity) do
+                                                       section.add new DefinitionArticle(mpropdef)
+                                               end
+                                       end
+                               end
+                       end
+                       root.add section
+               end
+       end
+
+       # Filters `self.mpropdefs` by `mmodule`.
+       #
+       # FIXME diff hack
+       private fun mproperties_for(mmodule: MModule): Set[MProperty] do
+               var mprops = new HashSet[MProperty]
+               for mpropdef in self.mpropdefs do
+                       if mpropdef.mclassdef.mmodule == mmodule then
+                               mprops.add mpropdef.mproperty
+                       end
+               end
+               return mprops
+       end
+
+       # Filters `self.mpropdefs` by `mproperty`.
+       #
+       # FIXME diff hack
+       private fun mpropdefs_for(mproperty: MProperty, mmodule: MModule): Set[MPropDef] do
+               var mpropdefs = new HashSet[MPropDef]
+               for mpropdef in self.mpropdefs do
+                       if mpropdef.mproperty == mproperty and
+                               mpropdef.mclassdef.mmodule == mmodule then
+                               mpropdefs.add mpropdef
+                       end
+               end
+               return mpropdefs
+       end
+end
+
+redef class MPropertyPage
+       redef fun apply_structure(v, doc) do
+               root.add new IntroArticle(mentity)
+               var concerns = self.concerns
+               if concerns == null or concerns.is_empty then return
+               # FIXME diff hack
+               mentity.intro.mclassdef.mmodule.mgroup.mproject.booster_rank = -1000
+               mentity.intro.mclassdef.mmodule.mgroup.booster_rank = -1000
+               mentity.intro.mclassdef.mmodule.booster_rank = -1000
+               concerns.sort_with(v.concerns_sorter)
+               mentity.intro.mclassdef.mmodule.mgroup.mproject.booster_rank = 0
+               mentity.intro.mclassdef.mmodule.mgroup.booster_rank = 0
+               mentity.intro.mclassdef.mmodule.booster_rank = 0
+               root.add new ConcernsArticle(mentity, concerns)
+               for mentity in concerns do
+                       var section = new ConcernSection(mentity)
+                       if mentity isa MModule then
+                               # Add mproperties
+                               var mpropdefs = mpropdefs_for(mentity).to_a
+                               v.name_sorter.sort(mpropdefs)
+                               for mpropdef in mpropdefs do
+                                       section.add new DefinitionArticle(mpropdef)
+                               end
+                       end
+                       root.add section
+               end
+       end
+
+       # Filters `self.mpropdefs` by `mmodule`.
+       private fun mpropdefs_for(mmodule: MModule): Set[MPropDef] do
+               var mpropdefs = new HashSet[MPropDef]
+               for mpropdef in self.mpropdefs do
+                       if mpropdef.mclassdef.mmodule == mmodule then
+                               mpropdefs.add mpropdef
+                       end
+               end
+               return mpropdefs
+       end
+end
+
+# A DocComposite element about a MEntity.
+class MEntityComposite
+       super DocComposite
+
+       # MEntity documented by this page element.
+       var mentity: MEntity
+end
+
+# A Section about a Concern.
+#
+# Those sections are used to build the page summary.
+class ConcernSection
+       super MEntityComposite
+       super DocSection
+end
+
+# An article about a Mentity.
+#
+# Used to display textual content about a MEntity.
+abstract class MEntityArticle
+       super MEntityComposite
+       super DocArticle
+end
+
+# An introduction article about a MEntity.
+#
+# Used at the top of a documentation page to introduce the documented MEntity.
+class IntroArticle
+       super MEntityComposite
+       super DocArticle
+end
+
+# An article that display a ConcernsTreee as a list.
+class ConcernsArticle
+       super MEntityArticle
+
+       # Concerns to list in this article.
+       var concerns: ConcernsTree
+end
+
+# An article that display the definition text of a MEntity.
+class DefinitionArticle
+       super MEntityArticle
+end
index e297f9c..bb0b3b3 100644 (file)
@@ -857,8 +857,15 @@ redef class AMethPropdef
                                return v.int_instance(args[0].to_i.bin_xor(args[1].to_i))
                        else if pname == "bin_not" then
                                return v.int_instance(args[0].to_i.bin_not)
+                       else if pname == "int_to_s_len" then
+                               return v.int_instance(recvval.to_s.length)
                        else if pname == "native_int_to_s" then
-                               return v.native_string_instance(recvval.to_s)
+                               var s = recvval.to_s
+                               var srecv = args[1].val.as(Buffer)
+                               srecv.clear
+                               srecv.append(s)
+                               srecv.add('\0')
+                               return null
                        else if pname == "strerror_ext" then
                                return v.native_string_instance(recvval.strerror)
                        end
@@ -963,13 +970,13 @@ redef class AMethPropdef
                                if fromval < 0 then
                                        debug("Illegal access on {recvval} for element {fromval}/{recvval.length}")
                                end
-                               if fromval + lenval >= recvval.length then
+                               if fromval + lenval > recvval.length then
                                        debug("Illegal access on {recvval} for element {fromval}+{lenval}/{recvval.length}")
                                end
                                if toval < 0 then
                                        debug("Illegal access on {destval} for element {toval}/{destval.length}")
                                end
-                               if toval + lenval >= destval.length then
+                               if toval + lenval > destval.length then
                                        debug("Illegal access on {destval} for element {toval}+{lenval}/{destval.length}")
                                end
                                recvval.as(FlatBuffer).copy(fromval, lenval, destval, toval)
index a046815..517deab 100644 (file)
@@ -330,8 +330,21 @@ redef class ModelBuilder
 
        # Return the mgroup associated to a directory path.
        # If the directory is not a group null is returned.
+       #
+       # Note: `paths` is also used to look for mgroups
        fun get_mgroup(dirpath: String): nullable MGroup
        do
+               if not dirpath.file_exists then do
+                       for p in paths do
+                               var try = p / dirpath
+                               if try.file_exists then
+                                       dirpath = try
+                                       break label
+                               end
+                       end
+                       return null
+               end label
+
                var rdp = module_absolute_path(dirpath)
                if mgroups.has_key(rdp) then
                        return mgroups[rdp]
@@ -527,6 +540,12 @@ redef class ModelBuilder
                nmodules.add(nmodule)
                self.mmodule2nmodule[mmodule] = nmodule
 
+               var source = nmodule.location.file
+               if source != null then
+                       assert source.mmodule == null
+                       source.mmodule = mmodule
+               end
+
                if decl != null then
                        # Extract documentation
                        var ndoc = decl.n_doc
@@ -673,6 +692,11 @@ redef class MGroup
 
 end
 
+redef class SourceFile
+       # Associated mmodule, once created
+       var mmodule: nullable MModule = null
+end
+
 redef class AStdImport
        # The imported module once determined
        var mmodule: nullable MModule = null
index c1a4851..333fa96 100644 (file)
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 # Documentation generator for the nit language.
+#
 # Generate API documentation in HTML format from nit source code.
 module nitdoc
 
@@ -20,21 +21,44 @@ import modelbuilder
 import doc
 
 redef class ToolContext
-       var docphase: Phase = new NitdocPhase(self, null)
+       # Nitdoc generation phase.
+       var docphase: Phase = new Nitdoc(self, null)
 end
 
-private class NitdocPhase
+# Nitdoc phase explores the model and generate pages for each mentities found
+private class Nitdoc
        super Phase
        redef fun process_mainmodule(mainmodule, mmodules)
        do
-               # generate doc
-               var nitdoc = new Nitdoc(toolcontext, mainmodule.model, mainmodule)
-               nitdoc.generate
+               var doc = new DocModel(mainmodule.model, mainmodule)
+
+               var phases = [
+                       new ExtractionPhase(toolcontext, doc),
+                       new IndexingPhase(toolcontext, doc),
+                       new MakePagePhase(toolcontext, doc),
+                       new POSetPhase(toolcontext, doc),
+                       new ConcernsPhase(toolcontext, doc),
+                       new StructurePhase(toolcontext, doc),
+                       new InheritanceListsPhase(toolcontext, doc),
+                       new IntroRedefListPhase(toolcontext, doc),
+                       new GraphPhase(toolcontext, doc),
+                       new RenderHTMLPhase(toolcontext, doc): DocPhase]
+
+               for phase in phases do
+                       toolcontext.info("# {phase.class_name}", 1)
+                       phase.apply
+               end
        end
 end
 
-# process options
+# build toolcontext
 var toolcontext = new ToolContext
+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
+
+# process options
 toolcontext.process_options(args)
 var arguments = toolcontext.option_context.rest
 
@@ -43,6 +67,7 @@ var model = new Model
 var mbuilder = new ModelBuilder(model, toolcontext)
 var mmodules = mbuilder.parse_full(arguments)
 
+# process
 if mmodules.is_empty then return
 mbuilder.run_phases
 toolcontext.run_global_phases(mmodules)