Merge: model_visitor: reject is_before and is_after as they are tests
authorJean Privat <jean@pryen.org>
Thu, 28 Sep 2017 23:39:22 +0000 (19:39 -0400)
committerJean Privat <jean@pryen.org>
Thu, 28 Sep 2017 23:39:22 +0000 (19:39 -0400)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

Pull-Request: #2554

37 files changed:
share/nitweb/directives/contributor-list.html [deleted file]
share/nitweb/directives/entity/card.html
share/nitweb/directives/search/card.html [deleted file]
share/nitweb/directives/search/field.html [deleted file]
share/nitweb/directives/ui/pagination.html [new file with mode: 0644]
share/nitweb/directives/ui/search-field.html
share/nitweb/index.html
share/nitweb/javascripts/catalog.js [new file with mode: 0644]
share/nitweb/javascripts/entities.js
share/nitweb/javascripts/grades.js
share/nitweb/javascripts/index.js [deleted file]
share/nitweb/javascripts/nitweb.js
share/nitweb/javascripts/ui.js
share/nitweb/stylesheets/cards.css
share/nitweb/stylesheets/nitweb.css
share/nitweb/stylesheets/search.css
share/nitweb/views/catalog/by_tags.html [deleted file]
share/nitweb/views/catalog/highlighted.html [deleted file]
share/nitweb/views/catalog/index.html
share/nitweb/views/catalog/most_required.html [deleted file]
share/nitweb/views/catalog/person.html [new file with mode: 0644]
share/nitweb/views/catalog/tag.html [new file with mode: 0644]
share/nitweb/views/doc/doc.html
share/nitweb/views/doc/entity.html
share/nitweb/views/doc/grades.html [new file with mode: 0644]
share/nitweb/views/search.html [new file with mode: 0644]
src/catalog.nit
src/doc/doc_commands.nit
src/doc/doc_down.nit
src/doc/doc_phases/doc_console.nit
src/doc/doc_phases/doc_readme.nit
src/doc/test_doc_commands.nit [new file with mode: 0644]
src/nitcatalog.nit
src/web/api_catalog.nit
src/web/api_docdown.nit
src/web/api_model.nit
src/web/web_base.nit

diff --git a/share/nitweb/directives/contributor-list.html b/share/nitweb/directives/contributor-list.html
deleted file mode 100644 (file)
index a46cce9..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div ng-if='listContributors.length > 0'>
-       <h3 id={{listId}}>
-               <span>{{listTitle}}</span>
-       </h3>
-       <ul class='list-unstyled user-list'>
-               <li ng-repeat='contributor in listContributors'>
-                       <img class='avatar' src="https://secure.gravatar.com/avatar/{{contributor.hash}}?size=20&amp;default=retro">
-                       {{contributor.name}}
-               </li>
-       </ul>
-</div>
index 0b48c22..18675a0 100644 (file)
@@ -1,35 +1,35 @@
 <div class='card'>
-       <div class='card-left text-center' ng-if='!noSynopsis'>
+       <div class='card-left text-center'>
                <entity-tag mentity='mentity' />
        </div>
-       <div class='card-body'>
-               <h5 class='card-heading' ng-if='!noSynopsis'>
-                       <entity-signature mentity='mentity'/>
+       <div class='card-body' ng-if='mentity.class_name == "MPackage"' style='width: 75%'>
+               <h5 class='card-heading'>
+                       <entity-signature mentity='mentity' />
+                       <small ng-if='mentity.class_name == "MPackage"'>
+                               <span ng-repeat='tag in mentity.metadata.tags'>
+                                       <a ui-sref='tag({id: tag})' class='text-muted'>{{tag}}</a>
+                                       <span ng-if='!$last'>,</span>
+                               </span>
+                       </small>
                </h5>
-               <div class='tab-content'>
-                       <div id='{{mentity.html_id}}-signature' class='tab-pane' ng-if='!noSynopsis'
-                         ng-class='currentTab == "signature" ? "active" : ""'>
-                               <span class='synopsis' ng-bind-html='mentity.mdoc.html_synopsis' />
-                       </div>
-                       <div id='{{mentity.html_id}}-grade' class='tab-pane'
-                         ng-class='currentTab == "grade" ? "active" : ""'>
-                               <entity-rating mentity='mentity' ratings='ratings'>
-                       </div>
-               </div>
+               <span class='synopsis' ng-bind-html='mentity.mdoc.html_synopsis' />
        </div>
-       <div class='card-right'>
-               <div class='dropdown'>
-                       <button class='btn btn-link dropdown-toggle' type='button' data-toggle='dropdown'>
-                               <span class='glyphicon glyphicon-chevron-down'></span>
-                       </button>
-                       <ul class='dropdown-menu dropdown-menu-right'>
-                               <li ng-class='currentTab == "signature" ? "active" : ""' ng-if='!noSynopsis'>
-                                       <a ng-click='currentTab = "signature"'>Signature</a>
-                               </li>
-                               <li ng-class='currentTab == "grade" ? "active" : ""'>
-                                       <a ng-click='loadEntityStars(); currentTab = "grade"'>Grade</a>
-                               </li>
-                       </ul>
-               </div>
+       <div class='card-body' ng-if='mentity.class_name != "MPackage"'>
+               <h5 class='card-heading'>
+                       <entity-signature mentity='mentity' />
+               </h5>
+               <span class='synopsis' ng-bind-html='mentity.mdoc.html_synopsis' />
+       </div>
+       <div class='card-right' ng-if='mentity.class_name == "MPackage"' style='width: 25%'>
+               <span ng-repeat='maintainer in mentity.metadata.maintainers'>
+                       <img class='avatar' src='https://secure.gravatar.com/avatar/{{maintainer.gravatar}}?size=14&amp;default=retro' />
+                       <a ui-sref='person({id: maintainer.name})'>{{maintainer.name}}</a>
+               </span>
+               <br>
+               <span ng-if='mentity.metadata.license'>
+                       <span class='text-muted'>
+                               <a href='http://opensource.org/licenses/{{mentity.license}}' class='text-muted'>{{mentity.metadata.license}}</a>
+                       </span>
+               </span>
        </div>
 </div>
diff --git a/share/nitweb/directives/search/card.html b/share/nitweb/directives/search/card.html
deleted file mode 100644 (file)
index 7052c60..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<div class='card card-search'>
-       <div class='card-left text-center'>
-               <entity-tag mentity='mentity' />
-       </div>
-       <div class='card-body'>
-               <h5 class='card-heading'>
-                       <entity-signature mentity='mentity'/>
-                       <br>
-                       <small><entity-namespace namespace='mentity.namespace' /></small>
-               </h5>
-               <span class='synopsis' ng-bind-html='mentity.mdoc.html_synopsis' />
-       </div>
-</div>
diff --git a/share/nitweb/directives/search/field.html b/share/nitweb/directives/search/field.html
deleted file mode 100644 (file)
index d9ee5f1..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-<form>
-       <div class='form-group has-icon'>
-               <input placeholder='Search...' type='text' class='form-control search-input'
-                       ng-model-options='{ debounce: 300 }' ng-model='query'
-                       ng-keydown='update($event)' ng-change='search()'>
-               <span class='glyphicon glyphicon-search form-control-icon text-muted'></span>
-       </div>
-       <div ng-if='results.length > 0' class='search-results'>
-               <div class='card-list'>
-                       <search-card ng-click='selectEnter()' ng-class='{active: activeItem == $index}' ng-mouseover='setActive($index)' mentity='mentity' ng-repeat='mentity in results' />
-               </div>
-       </div>
-</form>
diff --git a/share/nitweb/directives/ui/pagination.html b/share/nitweb/directives/ui/pagination.html
new file mode 100644 (file)
index 0000000..4af87d2
--- /dev/null
@@ -0,0 +1,14 @@
+<nav>
+       <ul class='pagination' ng-if='pagination.pagination.max > 1'>
+               <li ng-class='{disabled: pagination.pagination.page <= 1}'>
+                       <a ng-click='pagination.changePage(pagination.pagination.page - 1, pagination.pagination.limit)'><span>&laquo;</span></a>
+               </li>
+               <li ng-repeat='page in pagination.pages'
+                       ng-class='{disabled: pagination.pagination.page == page}'>
+                       <a ng-click='pagination.changePage(page, pagination.pagination.limit)'>{{page}}</a>
+               </li>
+               <li ng-class='{disabled: pagination.pagination.page >= pagination.pagination.max}'>
+                       <a ng-click='pagination.changePage(pagination.pagination.page + 1, pagination.pagination.limit)'><span>&raquo;</span></a>
+               </li>
+       </ul>
+</nav>
index e0bd4fd..a4d06a6 100644 (file)
@@ -2,7 +2,15 @@
        <div class='form-group has-icon'>
                <input placeholder='Search...' type='text' class='form-control search-input'
                        ng-model-options='{ debounce: 300 }' ng-model='vm.query'
-                       ng-keydown='update($event)' ng-change='vm.search()'>
+                       ng-keydown='vm.update($event)' ng-change='vm.search()'>
                <span class='glyphicon glyphicon-search form-control-icon text-muted'></span>
        </div>
+       <div ng-if='vm.results.results.length > 0' class='card-list search-results'>
+               <entity-card ng-click='vm.selectEnter()' ng-class='{active: vm.activeItem == $index}' ng-mouseover='vm.setActive($index)' mentity='mentity' ng-repeat='mentity in vm.results.results' />
+               <div class='card' ng-click='vm.selectEnter()' ng-mouseover='vm.setActive(vm.results.results.length)' ng-class='{active: vm.activeItem == vm.results.results.length}'>
+                       <div class='card-body'>
+                               Show all {{vm.results.total}} results for <a>"{{vm.query}}"</a>
+                       </div>
+               </div>
+       </div>
 </form>
index 2812b99..92eb64e 100644 (file)
@@ -47,7 +47,7 @@
                                        </div>
                                </div>
                                <div class='col-xs-7'>
-                                       <search-field />
+                                       <ui-search-field />
                                </div>
                                <div class='col-xs-2'>
                                        <user-menu />
@@ -73,7 +73,7 @@
                <script src='/javascripts/nitweb.js'></script>
                <script src='/javascripts/entities.js'></script>
                <script src='/javascripts/ui.js'></script>
-               <script src='/javascripts/index.js'></script>
+               <script src='/javascripts/catalog.js'></script>
                <script src='/javascripts/docdown.js'></script>
                <script src='/javascripts/metrics.js'></script>
                <script src='/javascripts/users.js'></script>
diff --git a/share/nitweb/javascripts/catalog.js b/share/nitweb/javascripts/catalog.js
new file mode 100644 (file)
index 0000000..2667db3
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016 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.
+ */
+
+(function() {
+       angular.module('catalog', [])
+
+       /* Router */
+
+       .config(function($stateProvider, $locationProvider) {
+               $stateProvider
+                       .state('catalog', {
+                               url: '/?p&n',
+                               controller: 'CatalogCtrl',
+                               controllerAs: 'vm',
+                               templateUrl: 'views/catalog/index.html',
+                               resolve: {
+                                       packages: function(Catalog, $q, $stateParams, $state) {
+                                               var d = $q.defer();
+                                               var page = $stateParams.p ? $stateParams.p : 1;
+                                               var limit = $stateParams.n ? $stateParams.n : 10;
+                                               Catalog.packages(page, limit, d.resolve,
+                                                       function(err) {
+                                                               $state.go('404', null, { location: false })
+                                                       });
+                                               return d.promise;
+                                       },
+                                       tags: function(Catalog, $q, $state) {
+                                               var d = $q.defer();
+                                               Catalog.tags(d.resolve,
+                                                       function(err) {
+                                                               $state.go('404', null, { location: false })
+                                                       });
+                                               return d.promise;
+                                       },
+                                       stats: function(Catalog, $q, $state) {
+                                               var d = $q.defer();
+                                               Catalog.stats(d.resolve,
+                                                       function(err) {
+                                                               $state.go('404', null, { location: false })
+                                                       });
+                                               return d.promise;
+
+                                       }
+                               }
+                       })
+                       .state('person', {
+                               url: '/person/:id?p1&n1&p2&n2',
+                               controller: 'PersonCtrl',
+                               controllerAs: 'vm',
+                               templateUrl: 'views/catalog/person.html',
+                               resolve: {
+                                       person: function(Catalog, $q, $stateParams, $state) {
+                                               var d = $q.defer();
+                                               Catalog.person($stateParams.id, d.resolve,
+                                               function(err) {
+                                                       $state.go('404', null, { location: false })
+                                               });
+                                               return d.promise;
+                                       },
+                                       maintaining: function(Catalog, $q, $stateParams, $state) {
+                                               var d = $q.defer();
+                                               var p1 = $stateParams.p2 ? $stateParams.p1 : 1;
+                                               var n1 = $stateParams.n2 ? $stateParams.n1 : 10;
+                                               Catalog.personMaintaining($stateParams.id, p1, n1, d.resolve,
+                                                       function(err) {
+                                                               $state.go('404', null, { location: false })
+                                                       });
+                                               return d.promise;
+                                       },
+                                       contributing: function(Catalog, $q, $stateParams, $state) {
+                                               var d = $q.defer();
+                                               var p2 = $stateParams.p2 ? $stateParams.p2 : 1;
+                                               var n2 = $stateParams.n2 ? $stateParams.n2 : 10;
+                                               Catalog.personContributing($stateParams.id, p2, n2, d.resolve,
+                                                       function(err) {
+                                                               $state.go('404', null, { location: false })
+                                                       });
+                                               return d.promise;
+
+                                       }
+                               }
+                       })
+                       .state('tag', {
+                               url: '/tag/:id?p&n',
+                               controller: 'TagCtrl',
+                               controllerAs: 'vm',
+                               templateUrl: 'views/catalog/tag.html',
+                               resolve: {
+                                       tag: function(Catalog, $q, $stateParams, $state) {
+                                               var d = $q.defer();
+                                               var page = $stateParams.p ? $stateParams.p : 1;
+                                               var limit = $stateParams.l ? $stateParams.l : 10;
+                                               Catalog.tag($stateParams.id, page, limit, d.resolve,
+                                                       function() {
+                                                               $state.go('404', null, { location: false })
+                                                       });
+                                               return d.promise;
+                                       }
+                               }
+                       })
+       })
+
+       /* Factories */
+
+       .factory('Catalog', [ '$http', function($http) {
+               return {
+                       stats: function(cb, cbErr) {
+                               $http.get('/api/catalog/stats')
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       packages: function(p, n, cb, cbErr) {
+                               $http.get('/api/catalog/packages?p=' + p + '&n=' + n)
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       tags: function(cb, cbErr) {
+                               $http.get('/api/catalog/tags')
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       person: function(id, cb, cbErr) {
+                               $http.get('/api/catalog/person/' + id)
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       personMaintaining: function(id, p, n, cb, cbErr) {
+                               $http.get('/api/catalog/person/' + id + '/maintaining?p=' + p + '&n=' + n)
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       personContributing: function(id, p, n, cb, cbErr) {
+                               $http.get('/api/catalog/person/' + id + '/contributing?p=' + p + '&n=' + n)
+                                       .success(cb)
+                                       .error(cbErr);
+                       },
+                       tag: function(id, p, n, cb, cbErr) {
+                               $http.get('/api/catalog/tag/' + id + '?p=' + p + '&n=' + n)
+                                       .success(cb)
+                                       .error(cbErr);
+                       }
+               };
+       }])
+
+       /* Controllers */
+
+       .controller('CatalogCtrl', function($scope, $state, packages, tags, stats) {
+               var vm = this;
+               vm.packages = packages;
+               vm.tags = tags;
+               vm.stats = stats;
+
+               $scope.$on('change-page', function(e, page, limit) {
+                       $state.go('catalog', {p: page, l: limit});
+               })
+       })
+
+       .controller('PersonCtrl', function($scope, $state, $stateParams, person, maintaining, contributing) {
+               var vm = this;
+               vm.person = person;
+               vm.maintaining = maintaining;
+               vm.contributing = contributing;
+
+               var p1 = $stateParams.p1 ? $stateParams.p1 : 1;
+               var n1 = $stateParams.n1 ? $stateParams.n1 : 10;
+               var p2 = $stateParams.p2 ? $stateParams.p2 : 1;
+               var n2 = $stateParams.n2 ? $stateParams.n2 : 10;
+
+               $scope.$on('change-page1', function(e, page, limit) {
+                       $state.go('person', {id: $stateParams.id, p1: page, n1: limit, p2: p2, n2: n2});
+               })
+
+               $scope.$on('change-page2', function(e, page, limit) {
+                       $state.go('person', {id: $stateParams.id, p1: p1, n1: n1, p2: page, n2: limit});
+               })
+       })
+
+       .controller('TagCtrl', function($state, $scope, tag) {
+               var vm = this;
+               vm.tag = tag;
+
+               $scope.$on('change-page', function(e, page, limit) {
+                       $state.go('tag', {id: vm.tag.tag, p: page, l: limit});
+               })
+       })
+})();
index c5deb0a..545e487 100644 (file)
                                        controller: function(mentity, doc) {
                                                this.mentity = mentity;
                                                this.doc = doc;
+
+                                               this.date = function(date) {
+                                                       return new Date(date);
+                                               }
                                        },
                                        controllerAs: 'vm',
                                })
                                                .success(cb)
                                                .error(cbErr);
                                },
-
-                               search: function(q, n, cb, cbErr) {
-                                       $http.get('/api/search?q=' + q + '&n=' + n)
+                               search: function(q, p, n, cb, cbErr) {
+                                       $http.get('/api/search?q=' + q + '&p=' + p + '&n=' + n)
                                                .success(cb)
                                                .error(cbErr);
                                }
                        return {
                                restrict: 'E',
                                scope: {
-                                       mentity: '=',
-                                       defaultTab: '@',
-                                       noSynopsis: '='
+                                       mentity: '='
                                },
                                replace: true,
-                               templateUrl: '/directives/entity/card.html',
-                               link: function ($scope, element, attrs) {
-                                       $scope.currentTab = $scope.defaultTab ? $scope.defaultTab : 'signature';
-
-                                       $scope.loadEntityStars = function() {
-                                               Feedback.loadEntityStars($scope.mentity.full_name,
-                                                       function(data) {
-                                                               $scope.ratings = data;
-                                                       }, function(message, status) {
-                                                               $scope.error = {message: message, status: status};
-                                                       });
-                                       };
-                               }
+                               templateUrl: '/directives/entity/card.html'
                        };
                }])
 
index 72eadbc..96ede05 100644 (file)
 
                .config(function($stateProvider, $locationProvider) {
                        $stateProvider
+                               .state('doc.entity.grades', {
+                                       url: '/grades',
+                                       templateUrl: 'views/doc/grades.html',
+                                       resolve: {
+                                               metrics: function(Feedback, $q, $stateParams, $state) {
+                                                       var d = $q.defer();
+                                                       Feedback.loadEntityStars($stateParams.id, d.resolve,
+                                                               function() {
+                                                                       $state.go('404', null, { location: false })
+                                                               });
+                                                       return d.promise;
+                                               }
+                                       },
+                                       controller: function(mentity, metrics) {
+                                               this.mentity = mentity;
+                                               this.metrics = metrics;
+                                       },
+                                       controllerAs: 'vm',
+                               })
                                .state('grades', {
                                        url: '/grades',
                                        templateUrl: 'views/grades.html',
diff --git a/share/nitweb/javascripts/index.js b/share/nitweb/javascripts/index.js
deleted file mode 100644 (file)
index 104d627..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright 2016 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.
- */
-
-(function() {
-       angular
-               .module('index', [])
-
-               .config(function($stateProvider, $locationProvider) {
-                       $stateProvider
-                               .state('catalog', {
-                                       url: '/',
-                                       templateUrl: 'views/catalog/index.html',
-                                       controller: 'CatalogCtrl',
-                                       controllerAs: 'vm',
-                                       abstract: true
-                               })
-                               .state('catalog.highlighted', {
-                                       url: '',
-                                       templateUrl: 'views/catalog/highlighted.html',
-                                       controller: 'CatalogHighlightedCtrl',
-                                       controllerAs: 'vm'
-                               })
-                               .state('catalog.required', {
-                                       url: 'required',
-                                       templateUrl: 'views/catalog/most_required.html',
-                                       controller: 'CatalogRequiredCtrl',
-                                       controllerAs: 'vm'
-                               })
-                               .state('catalog.tags', {
-                                       url: 'tags',
-                                       templateUrl: 'views/catalog/by_tags.html',
-                                       controller: 'CatalogTagsCtrl',
-                                       controllerAs: 'vm'
-                               })
-               })
-
-               .factory('Catalog', [ '$http', function($http) {
-                       return {
-                               loadHightlighted: function(cb, cbErr) {
-                                       $http.get('/api/catalog/highlighted')
-                                               .success(cb)
-                                               .error(cbErr);
-                               },
-
-                               loadMostRequired: function(cb, cbErr) {
-                                       $http.get('/api/catalog/required')
-                                               .success(cb)
-                                               .error(cbErr);
-                               },
-
-                               loadByTags: function(cb, cbErr) {
-                                       $http.get('/api/catalog/bytags')
-                                               .success(cb)
-                                               .error(cbErr);
-                               },
-
-                               loadStats: function(cb, cbErr) {
-                                       $http.get('/api/catalog/stats')
-                                               .success(cb)
-                                               .error(cbErr);
-                               },
-
-                               loadContributors: function(cb, cbErr) {
-                                       $http.get('/api/catalog/contributors')
-                                               .success(cb)
-                                               .error(cbErr);
-                               },
-                       }
-               }])
-
-               .controller('CatalogCtrl', function(Catalog) {
-                       var vm = this;
-
-                       Catalog.loadContributors(
-                               function(data) {
-                                       vm.contributors = data;
-                               }, function(err) {
-                                       vm.error = err;
-                               });
-
-                       Catalog.loadStats(
-                               function(data) {
-                                       vm.stats = data;
-                               }, function(err) {
-                                       vm.error = err;
-                               });
-               })
-
-               .controller('CatalogHighlightedCtrl', function(Catalog) {
-                       var vm = this;
-
-                       Catalog.loadHightlighted(
-                               function(data) {
-                                       vm.highlighted = data;
-                               }, function(err) {
-                                       vm.error = err;
-                               });
-               })
-
-               .controller('CatalogRequiredCtrl', function(Catalog) {
-                       var vm = this;
-
-                       Catalog.loadMostRequired(
-                               function(data) {
-                                       vm.required = data;
-                               }, function(err) {
-                                       vm.error = err;
-                               });
-               })
-
-               .controller('CatalogTagsCtrl', function(Catalog, $anchorScroll, $location) {
-                       var vm = this;
-
-                       Catalog.loadByTags(
-                               function(data) {
-                                       vm.bytags = data;
-                               }, function(err) {
-                                       vm.error = err;
-                               });
-
-
-                       vm.scrollTo = function(hash) {
-                               $location.hash(hash);
-                               $anchorScroll();
-                       }
-               })
-
-               .directive('contributorList', function(Model) {
-                       return {
-                               restrict: 'E',
-                               scope: {
-                                       listId: '@',
-                                       listTitle: '@',
-                                       listContributors: '='
-                               },
-                               templateUrl: '/directives/contributor-list.html'
-                       };
-               })
-})();
index 181da8e..1de3f69 100644 (file)
  */
 
 (function() {
-       angular.module('nitweb', ['ui.router', 'ngSanitize', 'angular-loading-bar', 'index', 'entities', 'docdown', 'metrics', 'users', 'grades'])
+       angular.module('nitweb', ['ui.router', 'ngSanitize', 'angular-loading-bar', 'catalog', 'entities', 'docdown', 'metrics', 'users', 'grades'])
 
        .config(['cfpLoadingBarProvider', function(cfpLoadingBarProvider) {
                cfpLoadingBarProvider.includeSpinner = false;
        }])
 
-       .run(['$anchorScroll', function($anchorScroll) {
+       .run(function($rootScope, $anchorScroll) {
                $anchorScroll.yOffset = 80;
-       }])
+               $rootScope.$on('$stateChangeSuccess', function() {
+                 $anchorScroll();
+               });
+       })
 
        .config(function($stateProvider, $locationProvider) {
                $stateProvider
index 7fc3f8a..5327782 100644 (file)
        angular
                .module('ui', [])
 
-               .controller('SearchCtrl', function(Model, $scope, $location, $document) {
-
-                       $scope.query = '';
-
-                       $scope.reset = function() {
-                               $scope.activeItem = 0;
-                               $scope.results = [];
-                       }
-
-                       $scope.update = function(e) {
-                               if(e.keyCode == 38) {
-                                       $scope.selectUp();
-                               } else if(e.keyCode == 40) {
-                                       $scope.selectDown();
-                               } else if(e.keyCode == 27) {
-                                       $scope.selectEscape();
-                               } else if(e.keyCode == 13) {
-                                       $scope.selectEnter();
-                               }
-                       }
-
-                       $scope.selectUp = function() {
-                               if($scope.activeItem > 0) {
-                                       $scope.activeItem -= 1;
-                               }
-                       }
-
-                       $scope.selectDown = function() {
-                               if($scope.activeItem < $scope.results.length - 1) {
-                                       $scope.activeItem += 1;
-                               }
-                       }
-
-                       $scope.selectEnter = function(e) {
-                               $location.url($scope.results[$scope.activeItem].web_url);
-                               $scope.reset();
-                       }
+               /* Search */
 
-                       $scope.selectEscape = function() {
-                               $scope.reset();
-                       }
-
-                       $scope.setActive = function(index) {
-                               $scope.activeItem = index;
-                       }
+               .config(function($stateProvider, $locationProvider) {
+                       $stateProvider
+                               .state('search', {
+                                       url: '/search?q&p&n',
+                                       controller: 'SearchCtrl',
+                                       controllerAs: 'vm',
+                                       templateUrl: 'views/search.html',
+                                       resolve: {
+                                               entities: function(Model, $q, $stateParams, $state) {
+                                                       var d = $q.defer();
+                                                       var query = $stateParams.q;
+                                                       var page = $stateParams.p ? $stateParams.p : 1;
+                                                       var limit = $stateParams.n ? $stateParams.n : 10;
+                                                       Model.search(query, page, limit, d.resolve,
+                                                               function() {
+                                                                       $state.go('404', null, { location: false })
+                                                               });
+                                                       return d.promise;
+                                               }
+                                       }
+                               })
+               })
 
-                       $scope.search = function() {
-                               if(!$scope.query) {
-                                       $scope.reset();
-                                       return;
-                               }
-                               Model.search($scope.query, 10,
-                                       function(data) {
-                                               $scope.reset();
-                                               $scope.results = data;
-                                       }, function(err) {
-                                               $scope.reset();
-                                               $scope.error = err;
-                                       });
-                       }
+               .controller('SearchCtrl', function($scope, $state, $stateParams, entities) {
+                       var vm = this;
+                       vm.entities = entities;
+                       vm.query = $stateParams.q;
 
-                       $scope.reset();
+                       $scope.$on('change-page', function(e, page, limit) {
+                               $state.go('search', {q: vm.query, p: page, l: limit});
+                       })
                })
 
-               .directive('searchField', function($document) {
+               .directive('uiSearchField', function($document) {
                        return {
                                restrict: 'E',
                                replace: true,
-                               controller: 'SearchCtrl',
-                               controllerAs: 'searchCtrl',
-                               templateUrl: '/directives/search/field.html',
-                               link: function ($scope, element, attrs) {
+                               controller: function($scope, $state, $stateParams, $location, Model) {
+                                       var vm = this;
+                                       vm.search = function() {
+                                               if(!vm.query) {
+                                                       vm.reset();
+                                                       return;
+                                               }
+                                               Model.search(vm.query, 1, 8,
+                                                       function(data) {
+                                                               vm.reset();
+                                                               vm.results = data;
+                                                       }, function(err) {
+                                                               vm.reset();
+                                                               vm.error = err;
+                                                       });
+                                       }
+
+                                       vm.reset = function() {
+                                               vm.activeItem = -1;
+                                               vm.results = {
+                                                       results: []
+                                               };
+                                       }
+
+                                       vm.update = function(e) {
+                                               if(e.keyCode == 38) {
+                                                       vm.selectUp();
+                                               } else if(e.keyCode == 40) {
+                                                       vm.selectDown();
+                                               } else if(e.keyCode == 27) {
+                                                       vm.selectEscape();
+                                               } else if(e.keyCode == 13) {
+                                                       vm.selectEnter();
+                                               }
+                                       }
+
+                                       vm.selectUp = function() {
+                                               if(vm.activeItem >= 0) {
+                                                       vm.activeItem -= 1;
+                                               }
+                                       }
+
+                                       vm.selectDown = function() {
+                                               if(vm.activeItem < vm.results.results.length) {
+                                                       vm.activeItem += 1;
+                                               }
+                                       }
+
+                                       vm.selectEnter = function(e) {
+                                               if(vm.activeItem >= 0 && vm.activeItem < vm.results.results.length) {
+                                                       $location.url(vm.results.results[vm.activeItem].web_url);
+                                               } else {
+                                                       $state.go('search', {q: vm.query, p: 1});
+                                               }
+                                               vm.reset();
+                                       }
+
+                                       vm.selectEscape = function() {
+                                               vm.reset();
+                                       }
+
+                                       vm.setActive = function(index) {
+                                               vm.activeItem = index;
+                                       }
+
+                                       vm.reset();
+
+                                       $scope.$watch(function() {
+                                               return $stateParams.q;
+                                       }, function(q) {
+                                               if(q) vm.query = q;
+                                       });
+                               },
+                               controllerAs: 'vm',
+                               templateUrl: 'directives/ui/search-field.html',
+                               link: function ($scope, element, attrs, ctrl) {
                                        $document.bind('click', function (event) {
                                                var isChild = $(element).has(event.target).length > 0;
                                                var isSelf = element[0] == event.target;
                                                var isInside = isChild || isSelf;
                                                if (!isInside) {
-                                                       $scope.reset();
+                                                       ctrl.reset();
                                                        $scope.$apply();
                                                }
                                        });
                        };
                })
 
-               .directive('searchCard', function() {
-                       return {
-                               restrict: 'E',
-                               scope: {
-                                       mentity: '='
-                               },
-                               replace: true,
-                               templateUrl: '/directives/search/card.html'
-                       };
-               })
+               /* Filters */
 
                .directive('uiFilters', function() {
                        return {
                                }
                        };
                })
+
+               /* Pagination */
+
+               .directive('uiPagination', function() {
+                       return {
+                               restrict: 'E',
+                               replace: true,
+                               bindToController: {
+                                       pagination: '=',
+                                       suffix: '=?'
+                               },
+                               controller: function($scope) {
+                                       var vm = this;
+
+                                       $scope.$watch('pagination.pagination', function(pagination) {
+                                               if(!pagination) return;
+                                               vm.computePages(pagination);
+                                       })
+
+                                       vm.computePages = function(pagination) {
+                                               vm.pages = [];
+                                               var len = 11;
+                                               var page = pagination.page;
+                                               var start = page - Math.floor(len / 2);
+                                               var end = page + Math.floor(len / 2);
+
+                                               if(start < 1) {
+                                                       end = Math.min(pagination.max, end + Math.abs(start) + 1)
+                                                       start = 1
+                                               } else if(end > pagination.max) {
+                                                       start = Math.max(1, start - Math.abs(end - pagination.max))
+                                                       end = pagination.max;
+                                               }
+
+                                               for(var i = start; i <= end; i++) {
+                                                       vm.pages.push(i);
+                                               }
+                                       }
+
+                                       vm.changePage = function(page, limit) {
+                                               if(page <= 0 || page > vm.pagination.max) return;
+                                               var suffix = vm.suffix ? vm.suffix : '';
+                                               $scope.$emit('change-page' + suffix, page, limit);
+                                       }
+                               },
+                               controllerAs: 'pagination',
+                               templateUrl: 'directives/ui/pagination.html'
+                       };
+               })
 })();
index 0d39859..de16f88 100644 (file)
@@ -29,9 +29,9 @@
        display: table;
        width: 100%;
        background: #fff;
-       border: 1px solid #ccc;
+       border: 1px solid #eee;
        margin-top: 10px;
-       box-shadow: 0 -1px 0 #e5e5e5,0 0 2px rgba(0,0,0,.12),0 2px 4px rgba(0,0,0,.24);
+       box-shadow: -1px -1px 3px rgba(0,0,0,.06), 1px 1px 3px rgba(0,0,0,.12);
 }
 
 .card-body, .card-left, .card-right {
@@ -57,7 +57,7 @@
 }
 
 .card-list > .card:first-child {
-       border-top: 1px solid #ccc;
+       border-top: 1px solid #ddd;
 }
 
 .card-list > .card {
index 864c296..ccfcdd0 100644 (file)
        background: #FF8100;
 }
 
+[ng-click] {
+       cursor: pointer;
+}
+
 /* Body */
 
 body {
@@ -127,28 +131,6 @@ entity-list:hover .btn-filter {
     pointer-events: none;
 }
 
-/* search */
-
-.search-input {
-       width: 100%;
-}
-
-.search-results {
-       position: absolute;
-       margin-top: 2px;
-       right: 15px;
-       left: 15px;
-}
-
-.search-results .card.active {
-       background: #eee;
-       border-color: #eee;
-}
-
-.card-search {
-       cursor: pointer;
-}
-
 /* navs */
 
 .nav-tabs li { cursor: pointer; }
index 2f6e2db..68516dc 100644 (file)
 .navbar-fixed-top *:-moz-placeholder { color: #fff; }
 .navbar-fixed-top *::-moz-placeholder { color: #fff; }
 .navbar-fixed-top *:-ms-input-placeholder { color: #fff; }
+
+.search-input {
+       width: 100%;
+}
+
+.card-list.search-results {
+       position: absolute;
+       margin-top: 2px;
+       right: 15px;
+       left: 15px;
+       box-shadow: 1px 1px 3px rgba(0,0,0,0.12), -1px -1px 3px rgba(0,0,0,.12);
+}
+
+.search-results .card.active {
+       background: #eee;
+       border-color: #eee;
+}
diff --git a/share/nitweb/views/catalog/by_tags.html b/share/nitweb/views/catalog/by_tags.html
deleted file mode 100644 (file)
index 31ca905..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<div>
-       <h3>Tags</h3>
-       <div class='container-fluid'>
-               <div class='col-xs-3' ng-repeat='(tag, packages) in vm.bytags'>
-                       <span class='badge'>{{packages.length}}</span>
-                       <a ng-click='vm.scrollTo(tag)'>{{tag}}</a>
-               </div>
-       </div>
-       <div ng-repeat='(tag, packages) in vm.bytags'>
-               <entity-list list-id='{{tag}}' list-title='{{tag}}'
-                       list-entities='packages'
-                       list-object-filter='{}' />
-       </div>
-</div>
diff --git a/share/nitweb/views/catalog/highlighted.html b/share/nitweb/views/catalog/highlighted.html
deleted file mode 100644 (file)
index 5607bd5..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<div>
-       <entity-list list-title='Highlighted packages'
-               list-entities='vm.highlighted'
-               list-object-filter='{}' />
-</div>
index 877db4a..2333a0a 100644 (file)
@@ -1,47 +1,35 @@
-<div class='container-fluid'>
+<div class='container'>
        <div class='page-header'>
                <h2>Welcome to NitWeb!</h2>
                <p class='text-muted'>The Nit knowledge base.</p>
        </div>
 
-       <ul class='nav nav-tabs' role='tablist'>
-               <li role='presentation' ui-sref-active='active'>
-                       <a ui-sref='catalog.highlighted'>
-                               <span class='glyphicon glyphicon-book'/> Highlighed
-                       </a>
-               </li>
-               <li role='presentation' ui-sref-active='active'>
-                       <a ui-sref='catalog.required'>
-                               <span class='glyphicon glyphicon-book'/> Most required
-                       </a>
-               </li>
-               <li role='presentation' ui-sref-active='active'>
-                       <a ui-sref='catalog.tags'>
-                               <span class='glyphicon glyphicon-book'/> By tags
-                       </a>
-               </li>
-       </ul>
-       <table class='table'>
-               <tr>
-                       <td ng-repeat='(key, value) in vm.stats'>
-                               <h5><strong>{{value}}</strong>&nbsp;<span>{{key}}</span></h5>
-                       </td>
-               </tr>
-       </table>
+       <div ng-if='vm.stats' class='container-fluid no-padding'>
+               <span ng-repeat='(key, value) in vm.stats' class='text-muted small'>
+                       <strong>{{value}}</strong>&nbsp;<span>{{key}}</span>
+                       &nbsp;
+               </span>
+       </div>
+       <hr/>
 
-       <div class='container-fluid'>
-               <div class='col-xs-9'>
-                       <div class='tab-content'>
-                               <div role='tabpanel' class='tab-pane fade in active'>
-                                       <ui-view />
-                               </div>
+       <div class='col-md-3 col-md-push-9 no-padding' ng-if='vm.tags'>
+               <h2>Tags</h2>
+               <div class='container-fluid no-padding'>
+                       <div class='col-xs-3 col-md-12' ng-repeat='(tag, packages) in vm.tags'>
+                               <span class='badge'>{{packages}}</span>
+                               <a ui-sref='tag({id: tag})'>{{tag}}</a>
                        </div>
                </div>
-               <div class='col-xs-3'>
-                       <contributor-list list-title='Maintainers'
-                                       list-contributors='vm.contributors.maintainers' />
-                       <contributor-list list-title='Contributors'
-                                       list-contributors='vm.contributors.contributors' />
+               <hr/>
+       </div>
+
+       <div class='col-md-9 col-md-pull-3 no-padding'>
+               <h2>Packages</h2>
+               <div class='card-list'>
+                       <entity-card mentity='package' ng-repeat='package in vm.packages.results' />
+               </div>
+               <div class='container text-center' ng-if='vm.packages'>
+                       <ui-pagination pagination='vm.packages'/>
                </div>
        </div>
 </div>
diff --git a/share/nitweb/views/catalog/most_required.html b/share/nitweb/views/catalog/most_required.html
deleted file mode 100644 (file)
index 93bf2e1..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<div>
-       <entity-list list-title='Most required'
-               list-entities='vm.required'
-               list-object-filter='{}' />
-</div>
diff --git a/share/nitweb/views/catalog/person.html b/share/nitweb/views/catalog/person.html
new file mode 100644 (file)
index 0000000..433b73d
--- /dev/null
@@ -0,0 +1,33 @@
+<div class='container'>
+       <div class='col-xs-2 no-padding'>
+               <img class='avatar' style='width:100%; max-width:100px' src='https://secure.gravatar.com/avatar/{{vm.person.gravatar}}?size=100&amp;default=retro' />
+       </div>
+       <div class='col-xs-10'>
+               <h1>
+                       {{vm.person.name}}<br>
+                       <small>{{vm.person.email}}</small>
+               </h1>
+       </div>
+       <hr/>
+</div>
+<br><br>
+<div class='container'>
+       <div ng-if='vm.maintaining.results.length > 0'>
+               <h3 id='maintaining'>{{vm.maintaining.total}} maintained projects</h3>
+               <div class='card-list'>
+                       <entity-card mentity='package' ng-repeat='package in vm.maintaining.results' />
+               </div>
+               <div class='container text-center' ng-if='vm.maintaining'>
+                       <ui-pagination pagination='vm.maintaining' suffix='1'/>
+               </div>
+       </div>
+       <div ng-if='vm.contributing.results.length > 0'>
+               <h3 id='contributing'>{{vm.contributing.total}} contributed projects</h3>
+               <div class='card-list'>
+                       <entity-card mentity='package' ng-repeat='package in vm.contributing.results' />
+               </div>
+               <div class='container text-center' ng-if='vm.contributing'>
+                       <ui-pagination pagination='vm.contributing' suffix='2' />
+               </div>
+       </div>
+</div>
diff --git a/share/nitweb/views/catalog/tag.html b/share/nitweb/views/catalog/tag.html
new file mode 100644 (file)
index 0000000..6c0a37e
--- /dev/null
@@ -0,0 +1,16 @@
+<div class='container'>
+       <h1>
+               {{vm.tag.tag}}
+               <small>{{vm.tag.packages.total}} packages</small>
+       </h1>
+       <hr/>
+
+       <div class='container-fluid no-padding' ng-if='vm.tag.packages.results.length > 0'>
+               <div class='card-list'>
+                       <entity-card mentity='package' ng-repeat='package in vm.tag.packages.results' />
+               </div>
+               <div class='container text-center' ng-if='vm.tag.packages'>
+                       <ui-pagination pagination='vm.tag.packages'/>
+               </div>
+       </div>
+</div>
index 1e60694..eadcf37 100644 (file)
@@ -1,8 +1,10 @@
 <div>
-       <div class='col-xs-3'>
+       <div class='col-lg-2 col-sm-3 col-xs-12'>
                <ui-summary target='#summary-content' />
        </div>
-       <div class='col-xs-9' id='summary-content'>
+       <div class='col-lg-10 col-sm-9 col-xs-12' id='summary-content' ng-class='{
+               "col-lg-8 col-sm-6 col-xs-12": vm.mentity.class_name == "MPackage"
+       }'>
                <div class='card'>
                        <div class='card-body'>
                                <div ng-if='vm.doc'>
                        list-object-filter='{is_init: "!true"}' />
 
        </div>
+
+       <div class='col-lg-2 col-sm-3 col-xs-12' ng-class='{
+               "hidden": vm.mentity.class_name != "MPackage"
+       }'>
+               <br>
+               <p ng-repeat='maintainer in vm.mentity.metadata.maintainers' class='lead'>
+                       <img class='avatar' src='https://secure.gravatar.com/avatar/{{maintainer.gravatar}}?size=20&default=retro' />
+                       <span>
+                               <a ui-sref='person({id: maintainer.name})'>{{maintainer.name}}</a>
+                       </span>
+                       <br>
+               </p>
+               <span ng-if='vm.mentity.metadata.license'>
+                       <span class='text-muted'>
+                               <a href='http://opensource.org/licenses/{{vm.mentity.metadata.license}}'>{{vm.mentity.metadata.license}}</a>
+                               license
+                       </span>
+                       <br>
+               </span>
+
+               <div ng-if='vm.mentity.metadata.homepage || vm.mentity.metadata.issues'>
+                       <h3>Links</h3>
+                       <ul class='list-unstyled'>
+                               <li ng-if='vm.mentity.metadata.homepage'>
+                                       <a href='{{vm.mentity.metadata.homepage}}'>Homepage</a>
+                               </li>
+                               <li ng-if='vm.mentity.metadata.browse'>
+                                       <a href='{{vm.mentity.metadata.browse}}'>Source Code</a>
+                               </li>
+                               <li ng-if='vm.mentity.metadata.issues'>
+                                       <a href='{{vm.mentity.metadata.issues}}'>Issues</a>
+                               </li>
+                       </ul>
+               </div>
+
+               <div ng-if='vm.mentity.metadata.git || vm.mentity.stats.commits'>
+                       <h3>Git</h3>
+                       <ul class='list-unstyled' style='white-space: nowrap; overflow: hidden; text-overflow: ellipsis;'>
+                               <li ng-if='vm.mentity.metadata.git'>
+                                       <a href='{{vm.mentity.metadata.git}}'>{{vm.mentity.metadata.git}}</a>
+                               </li>
+                               <li ng-if='vm.mentity.stats.commits' class='text-muted'>
+                                       <br><b>{{vm.mentity.stats.commits}} commits</b>
+                               </li>
+                               <li ng-if='vm.mentity.metadata.last_date'><b class='text-muted'>Last:</b> {{vm.date(vm.mentity.metadata.last_date) | date: 'medium'}}</li>
+                               <li ng-if='vm.mentity.metadata.first_date'><b class='text-muted'>First: </b>{{vm.date(vm.mentity.metadata.first_date) | date: 'medium'}}</li>
+                       </ul>
+               </div>
+
+               <div ng-if='vm.mentity.stats'>
+                       <h3>Quality</h3>
+                       <ul class='list-unstyled'>
+                               <li ng-if='vm.mentity.stats.documentation_score'>
+                                       {{vm.mentity.stats.documentation_score}}% documented
+                               </li>
+                               <li ng-if='vm.mentity.stats.errors' class='text-danger'>
+                                       {{vm.mentity.stats.errors}} errors
+                               </li>
+                               <li ng-if='vm.mentity.stats.warnings' class='text-warning'>
+                                       {{vm.mentity.stats.warnings}} warnings
+                                       ({{vm.mentity.stats.warnings_per_kloc}} / kloc)
+                               </li>
+                       </ul>
+               </div>
+
+               <div ng-if='vm.mentity.metadata.tags.length > 0'>
+                       <h3>Tags</h3>
+                       <span ng-repeat='tag in vm.mentity.metadata.tags'>
+                               <a ui-sref='tag({id: tag})'>{{tag}}</a><span ng-if='!$last'>,</span>
+                       </span>
+               </div>
+
+               <div ng-if='vm.mentity.dependencies.length > 0'>
+                       <h3>Requirements</h3>
+                       <span ng-repeat='parent in vm.mentity.dependencies'>
+                               <a ui-sref='mentity({id: parent.name})' title='{{parent.synopsis}}'>{{parent.name}}</a><span ng-if='!$last'>,</span>
+                       </span>
+               </div>
+
+               <div ng-if='vm.mentity.clients.length > 0'>
+                       <h3>Clients</h3>
+                       <span ng-repeat='client in vm.mentity.clients'>
+                               <a ui-sref='mentity({id: client.name})' title='{{client.synopsis}}'>{{client.name}}</a><span ng-if='!$last'>,</span>
+                       </span>
+               </div>
+
+               <div ng-if='vm.mentity.metadata.contributors.length > 1'>
+                       <h3>Contributors</h3>
+                       <ul class='list-unstyled'>
+                               <li ng-repeat='contributor in vm.mentity.metadata.contributors'>
+                                       <img class='avatar' src='https://secure.gravatar.com/avatar/{{contributor.gravatar}}?size=20&default=retro' />
+                                       <a ui-sref='person({id: contributor.name})'>
+                                       {{contributor.name}}</a>
+                               </li>
+                       </ul>
+               </div>
+
+               <div ng-if='vm.mentity.stats'>
+                       <h3>Stats</h3>
+                       <ul class='list-unstyled'>
+                               <li>{{vm.mentity.stats.mmodules}} modules</li>
+                               <li>{{vm.mentity.stats.mclasses}} classes</li>
+                               <li>{{vm.mentity.stats.mmethods}} methods</li>
+                               <li>{{vm.mentity.stats.loc}} loc</li>
+                       </ul>
+               </div>
+       </div>
 </div>
index 3543b9f..e6b559f 100644 (file)
                                <span class='glyphicon glyphicon-stats'/> Metrics
                        </a>
                </li>
+
+               <!-- grades -->
+               <li role='presentation' ui-sref-active='active'>
+                       <a ui-sref='.grades'>
+                               <span class='glyphicon glyphicon-star'/> Grades
+                       </a>
+               </li>
        </ul>
        <br>
        <ui-view />
diff --git a/share/nitweb/views/doc/grades.html b/share/nitweb/views/doc/grades.html
new file mode 100644 (file)
index 0000000..89fc74c
--- /dev/null
@@ -0,0 +1,5 @@
+<div class='card'>
+       <div class='card-body'>
+               <entity-rating mentity='vm.mentity' ratings='vm.ratings'>
+       </div>
+</div>
diff --git a/share/nitweb/views/search.html b/share/nitweb/views/search.html
new file mode 100644 (file)
index 0000000..6614e9c
--- /dev/null
@@ -0,0 +1,12 @@
+<div class='container'>
+       <h2>
+               {{vm.entities.total}} matches for
+               <a ui-sref='search({q: vm.query})'>{{vm.query}}</a>
+       </h2>
+       <div class='card-list'>
+               <entity-card mentity='mentity' ng-repeat='mentity in vm.entities.results' />
+       </div>
+       <div class='container text-center' ng-if='vm.entities'>
+               <ui-pagination pagination='vm.entities'/>
+       </div>
+</div>
index 51cdd53..dd891ac 100644 (file)
@@ -53,18 +53,41 @@ import counter # For statistics
 import modelize # To process and count classes and methods
 
 redef class MPackage
+
+       # Metadata related to this package
+       var metadata = new MPackageMetadata(self)
+end
+
+# The metadata extracted from a MPackage
+class MPackageMetadata
+
+       # The mpacakge this metadata belongs to
+       var mpackage: MPackage
+
        # Return the associated metadata from the `ini`, if any
-       fun metadata(key: String): nullable String
-       do
-               var ini = self.ini
+       fun metadata(key: String): nullable String do
+               var ini = mpackage.ini
                if ini == null then return null
                return ini[key]
        end
 
        # The consolidated list of tags
-       var tags = new Array[String]
+       var tags: Array[String] is lazy do
+               var tags = new Array[String]
+               var string = metadata("package.tags")
+               if string == null then return tags
+               for tag in string.split(",") do
+                       tag = tag.trim
+                       if tag.is_empty then continue
+                       tags.add tag
+               end
+               if tryit != null then tags.add "tryit"
+               if apk != null then tags.add "apk"
+               if tags.is_empty then tags.add "none"
+               return tags
+       end
 
-       # The list of maintainers
+       # The list of all maintainers
        var maintainers = new Array[Person]
 
        # The list of contributors
@@ -75,6 +98,43 @@ redef class MPackage
 
        # The date of the oldest commit
        var first_date: nullable String = null
+
+       # Key: package.maintainer`
+       var maintainer: nullable String is lazy do return metadata("package.maintainer")
+
+       # Key: `package.more_contributors`
+       var more_contributors: Array[String] is lazy do
+               var res = new Array[String]
+               var string = metadata("package.more_contributors")
+               if string == null then return res
+               for c in string.split(",") do
+                       c = c.trim
+                       if c.is_empty then continue
+                       res.add c
+               end
+               return res
+       end
+
+       # Key: `package.license`
+       var license: nullable String is lazy do return metadata("package.license")
+
+       # Key: `upstream.tryit`
+       var tryit: nullable String is lazy do return metadata("upstream.tryit")
+
+       # Key: `upstream.apk`
+       var apk: nullable String is lazy do return metadata("upstream.apk")
+
+       # Key: `upstream.homepage`
+       var homepage: nullable String is lazy do return metadata("upstream.homepage")
+
+       # Key: `upstream.browse`
+       var browse: nullable String is lazy do return metadata("upstream.browse")
+
+       # Package git clone address
+       var git: nullable String is lazy do return metadata("upstream.git")
+
+       # Package issue tracker
+       var issues: nullable String is lazy do return metadata("upstream.issues")
 end
 
 redef class Int
@@ -98,6 +158,13 @@ class Person
        # Some homepage. Eg "http://example.com/~jdoe"
        var page: nullable String is writable
 
+       # Gravatar id
+       var gravatar: nullable String is lazy do
+               var email = self.email
+               if email == null then return null
+               return email.md5.to_lower
+       end
+
        # Return a full-featured link to a person
        fun to_html: String
        do
@@ -107,10 +174,9 @@ class Person
                if page != null then
                        res += "<a href=\"{page.html_escape}\">"
                end
-               var email = self.email
-               if email != null then
-                       var md5 = email.md5.to_lower
-                       res += "<img src=\"https://secure.gravatar.com/avatar/{md5}?size=20&amp;default=retro\">&nbsp;"
+               var gravatar = self.gravatar
+               if gravatar != null then
+                       res += "<img src=\"https://secure.gravatar.com/avatar/{gravatar}?size=20&amp;default=retro\">&nbsp;"
                end
                res += e
                if page != null then res += "</a>"
@@ -202,6 +268,9 @@ class Catalog
        # used to access the files and count source lines of code
        var modelbuilder: ModelBuilder
 
+       # List of all packages by their names
+       var mpackages = new HashMap[String, MPackage]
+
        # Packages by tag
        var tag2proj = new MultiHashMap[String, MPackage]
 
@@ -249,9 +318,15 @@ class Catalog
        # The score is loosely computed using other metrics
        var score = new Counter[MPackage]
 
-       # List of known people
+       # List of known people by their git string
        var persons = new HashMap[String, Person]
 
+       # Map person short names to person objects
+       var name2person = new HashMap[String, Person]
+
+       # Package statistics cache
+       var mpackages_stats = new HashMap[MPackage, MPackageStats]
+
        # Scan, register and add a contributor to a package
        fun register_contrib(person: String, mpackage: MPackage): Person
        do
@@ -268,14 +343,17 @@ class Catalog
                var projs = contrib2proj[p]
                if not projs.has(mpackage) then
                        projs.add mpackage
-                       mpackage.contributors.add p
+                       mpackage.metadata.contributors.add p
                end
+               name2person[p.name] = p
                return p
        end
 
        # Compute information for a package
        fun package_page(mpackage: MPackage)
        do
+               mpackages[mpackage.full_name] = mpackage
+
                var score = score[mpackage].to_f
 
                var mdoc = mpackage.mdoc_or_fallback
@@ -283,58 +361,45 @@ class Catalog
                        score += 100.0
                        score += mdoc.content.length.score
                end
+               var metadata = mpackage.metadata
 
-
-               var tryit = mpackage.metadata("upstream.tryit")
+               var tryit = metadata.tryit
                if tryit != null then
                        score += 1.0
                end
-               var apk = mpackage.metadata("upstream.apk")
+               var apk = metadata.apk
                if apk != null then
                        score += 1.0
                end
-
-               var homepage = mpackage.metadata("upstream.homepage")
+               var homepage = metadata.homepage
                if homepage != null then
                        score += 5.0
                end
-               var maintainer = mpackage.metadata("package.maintainer")
+               var maintainer = metadata.maintainer
                if maintainer != null then
                        score += 5.0
                        var person = register_contrib(maintainer, mpackage)
-                       mpackage.maintainers.add person
+                       mpackage.metadata.maintainers.add person
                        var projs = maint2proj[person]
                        if not projs.has(mpackage) then projs.add mpackage
                end
-               var license = mpackage.metadata("package.license")
+               var license = metadata.license
                if license != null then
                        score += 5.0
                end
-
-               var browse = mpackage.metadata("upstream.browse")
+               var browse = metadata.browse
                if browse != null then
                        score += 5.0
                end
-
-               var tags = mpackage.metadata("package.tags")
-               var ts = mpackage.tags
-               if tags != null then
-                       for t in tags.split(",") do
-                               t = t.trim
-                               if t == "" then continue
-                               ts.add t
-                       end
+               var tags = metadata.tags
+               for tag in tags do
+                       tag2proj[tag].add mpackage
                end
-               if ts.is_empty then ts.add "none"
-               if tryit != null then ts.add "tryit"
-               if apk != null then ts.add "apk"
-               for t in ts do
-                       tag2proj[t].add mpackage
+               if tags.not_empty then
+                       var cat = tags.first
+                       cat2proj[cat].add mpackage
+                       score += tags.length.score
                end
-               var cat = ts.first
-               cat2proj[cat].add mpackage
-               score += ts.length.score
-
                if deps.has(mpackage) then
                        score += deps[mpackage].greaters.length.score
                        score += deps[mpackage].direct_greaters.length.score
@@ -342,15 +407,12 @@ class Catalog
                        score += deps[mpackage].direct_smallers.length.score
                end
 
-               var contributors = mpackage.contributors
-               var more_contributors = mpackage.metadata("package.more_contributors")
-               if more_contributors != null then
-                       for c in more_contributors.split(",") do
-                               register_contrib(c.trim, mpackage)
-                       end
+               var contributors = mpackage.metadata.contributors
+               var more_contributors = metadata.more_contributors
+               for c in more_contributors do
+                       register_contrib(c, mpackage)
                end
                score += contributors.length.to_f
-
                var mmodules = 0
                var mclasses = 0
                var mmethods = 0
@@ -417,7 +479,6 @@ class Catalog
                end
                var documentation_score =  (100.0 * doc_score / entity_score).to_i
                self.documentation_score[mpackage] = documentation_score
-
                #score += mmodules.score
                score += mclasses.score
                score += mmethods.score
@@ -433,12 +494,15 @@ class Catalog
                var ini = mpackage.ini
                if ini == null then return
 
+               var root = mpackage.root
+               if root == null then return
+
                # TODO use real git info
                #var repo = ini.get_or_null("upstream.git")
                #var branch = ini.get_or_null("upstream.git.branch")
                #var directory = ini.get_or_null("upstream.git.directory")
 
-               var dirpath = mpackage.root.filepath
+               var dirpath = root.filepath
                if dirpath == null then return
 
                # Collect commits info
@@ -452,8 +516,8 @@ class Catalog
                        if s.length != 2 or s.last == "" then continue
 
                        # Collect date of last and first commit
-                       if mpackage.last_date == null then mpackage.last_date = s.first
-                       mpackage.first_date = s.first
+                       if mpackage.metadata.last_date == null then mpackage.metadata.last_date = s.first
+                       mpackage.metadata.first_date = s.first
 
                        # Count contributors
                        contributors.inc(s.last)
@@ -461,10 +525,132 @@ class Catalog
                for c in contributors.sort.reverse_iterator do
                        register_contrib(c, mpackage)
                end
+       end
+
+       # Compose package stats
+       fun mpackage_stats(mpackage: MPackage): MPackageStats do
+               var stats = new MPackageStats
+               stats.mmodules = mmodules[mpackage]
+               stats.mclasses = mclasses[mpackage]
+               stats.mmethods = mmethods[mpackage]
+               stats.loc = loc[mpackage]
+               stats.errors = errors[mpackage]
+               stats.warnings = warnings[mpackage]
+               stats.warnings_per_kloc = warnings_per_kloc[mpackage]
+               stats.documentation_score = documentation_score[mpackage]
+               stats.commits = commits[mpackage]
+               stats.score = score[mpackage]
+
+               mpackages_stats[mpackage] = stats
+               return stats
+       end
 
+       # Compose catalog stats
+       var catalog_stats: CatalogStats is lazy do
+               var stats = new CatalogStats
+               stats.packages = mpackages.length
+               stats.maintainers = maint2proj.length
+               stats.contributors = contrib2proj.length
+               stats.tags = tag2proj.length
+               stats.modules = mmodules.sum
+               stats.classes = mclasses.sum
+               stats.methods = mmethods.sum
+               stats.loc = loc.sum
+               return stats
        end
 end
 
+# Catalog statistics
+class CatalogStats
+
+       # Number of packages
+       var packages = 0
+
+       # Number of maintainers
+       var maintainers = 0
+
+       # Number of contributors
+       var contributors = 0
+
+       # Number of tags
+       var tags = 0
+
+       # Number of modules
+       var modules = 0
+
+       # Number of classes
+       var classes = 0
+
+       # Number of methods
+       var methods = 0
+
+       # Number of line of codes
+       var loc = 0
+end
+
+# MPackage statistics for the catalog
+class MPackageStats
+
+       # Number of modules
+       var mmodules = 0
+
+       # Number of classes
+       var mclasses = 0
+
+       # Number of methods
+       var mmethods = 0
+
+       # Number of lines of code
+       var loc = 0
+
+       # Number of errors
+       var errors = 0
+
+       # Number of warnings and advices
+       var warnings = 0
+
+       # Number of warnings per 1000 lines of code (w/kloc)
+       var warnings_per_kloc = 0
+
+       # Documentation score (between 0 and 100)
+       var documentation_score = 0
+
+       # Number of commits by package
+       var commits = 0
+
+       # Score by package
+       #
+       # The score is loosely computed using other metrics
+       var score = 0
+end
+
+# Sort the mpackages by their score
+class CatalogScoreSorter
+       super Comparator
+
+       # Catalog used to access scores
+       var catalog: Catalog
+
+       redef type COMPARED: MPackage
+
+       redef fun compare(a, b) do
+               if not catalog.mpackages_stats.has_key(a) then return 1
+               if not catalog.mpackages_stats.has_key(b) then return -1
+               var astats = catalog.mpackages_stats[a]
+               var bstats = catalog.mpackages_stats[b]
+               return bstats.score <=> astats.score
+       end
+end
+
+# Sort tabs alphabetically
+class CatalogTagsSorter
+       super Comparator
+
+       redef type COMPARED: String
+
+       redef fun compare(a, b) do return a <=> b
+end
+
 # Execute a git command and return the result
 fun git_run(command: String...): String
 do
index 1537704..208738b 100644 (file)
 # * `nitdoc` wikilinks like `[[doc: MEntity::name]]`
 module doc_commands
 
-# A command aimed at a documentation tool like `nitdoc` or `nitx`.
 #
-# `DocCommand` are generally of the form `command: args`.
-interface DocCommand
+class DocCommandParser
 
-       # Original command string.
-       fun string: String is abstract
+       # List of allowed command names for this parser
+       var allowed_commands: Array[String] = [ "doc", "list", "param", "return",
+               "new", "call", "code", "graph"] is writable
 
-       # Command name.
-       fun name: String is abstract
-
-       # Command arguments.
+       # Parse `string` as a DocCommand
        #
-       # FIXME: define a syntax
-       fun args: Array[String] is abstract
-
-       # Command factory.
+       # Returns `null` if the string cannot be parsed.
+       #
+       # ~~~
+       # var parser = new DocCommandParser
        #
-       # Returns a concrete instance of `DocCommand` depending on the string.
-       new(command_string: String) do
-               if command_string.has_prefix("doc:") then
-                       return new ArticleCommand(command_string)
-               else if command_string.has_prefix("comment:") then
-                       return new CommentCommand(command_string)
-               else if command_string.has_prefix("list:") then
-                       return new ListCommand(command_string)
-               else if command_string.has_prefix("param:") then
-                       return new ParamCommand(command_string)
-               else if command_string.has_prefix("return:") then
-                       return new ReturnCommand(command_string)
-               else if command_string.has_prefix("new:") then
-                       return new NewCommand(command_string)
-               else if command_string.has_prefix("call:") then
-                       return new CallCommand(command_string)
-               else if command_string.has_prefix("code:") then
-                       return new CodeCommand(command_string)
-               else if command_string.has_prefix("graph:") then
-                       return new GraphCommand(command_string)
+       # var command = parser.parse("doc: core::Array")
+       # assert command isa CommentCommand
+       # assert command.arg == "core::Array"
+       #
+       # command = parser.parse(":") # syntax error
+       # assert command == null
+       # assert parser.errors.not_empty
+       # ~~~
+       fun parse(string: String): nullable DocCommand do
+               var pos = 0
+               var tmp = new FlatBuffer
+               errors.clear
+
+               # Parse command name
+               pos = string.read_until(tmp, pos, ':')
+               var name = tmp.write_to_string.trim
+
+               # Check allowed commands
+               if name.is_empty then
+                       error("empty command name", 0)
+                       return null
+               end
+               if not allowed_commands.has(name) then
+                       error("unknown command name", 0)
+                       return null
+               end
+
+               # Build the command
+               var command = new_command(name, string)
+               if command == null then
+                       error("unknown command name", 0)
+                       return null
+               end
+
+               # Parse the argument
+               tmp.clear
+               pos = string.read_until(tmp, pos + 1, '|')
+               var arg = tmp.write_to_string.trim
+               if arg.is_empty then
+                       error("empty command arg", pos)
+                       return null
+               end
+               command.arg = arg
+
+               # Parse command options
+               while pos < string.length do
+                       # Parse option name
+                       tmp.clear
+                       pos = string.read_until(tmp, pos + 1, ':', ',')
+                       var oname = tmp.write_to_string.trim
+                       var oval = ""
+                       if oname.is_empty then break
+                       # Parse option value
+                       if pos < string.length and string[pos] == ':' then
+                               tmp.clear
+                               pos = string.read_until(tmp, pos + 1, ',')
+                               oval = tmp.write_to_string.trim
+                       end
+                       command.opts[oname] = oval
+                       # TODO Check options
                end
-               return new UnknownCommand(command_string)
+
+               return command
+       end
+
+       # Init a new DocCommand from its `name`
+       #
+       # You must redefine this method to add new custom commands.
+       fun new_command(name, string: String): nullable DocCommand do
+               if name == "doc" then return new CommentCommand(string)
+               if name == "list" then return new ListCommand(string)
+               if name == "param" then return new ParamCommand(string)
+               if name == "return" then return new ReturnCommand(string)
+               if name == "new" then return new NewCommand(string)
+               if name == "call" then return new CallCommand(string)
+               if name == "code" then return new CodeCommand(string)
+               if name == "graph" then return new GraphCommand(string)
+               return null
        end
 
-       redef fun to_s do return string
+       # Errors and warnings from last call to `parse`
+       var errors = new Array[DocMessage]
+
+       # Generate an error
+       fun error(message: String, col: nullable Int) do
+               errors.add new DocMessage(1, message, col)
+       end
+
+       # Generate a warning
+       fun warning(message: String, col: nullable Int) do
+               errors.add new DocMessage(2, message, col)
+       end
 end
 
-# Used to factorize initialization of DocCommands.
-abstract class AbstractDocCommand
-       super DocCommand
+# A message generated by the DocCommandParser
+class DocMessage
+
+       # Message severity
+       #
+       # 1- Error
+       # 2- Warning
+       var level: Int
+
+       # Message explanatory string
+       var message: String
 
-       redef var string
-       redef var name is noinit
-       redef var args = new Array[String]
+       # Related column in original string if any
+       var col: nullable Int
 
-       init do
-               # parse command
+       redef fun to_s do
                var str = new FlatBuffer
-               var i = 0
-               while i < string.length do
-                       var c = string[i]
-                       i += 1
-                       if c == ':' then break
-                       str.add c
+               if level == 1 then
+                       str.append "Error: "
+               else
+                       str.append "Warning: "
                end
-               name = str.write_to_string
-               # parse args
-               args.add string.substring_from(i).trim
+               str.append message
+               var col = self.col
+               if col != null then
+                       str.append " (col: {col})"
+               end
+               return str.write_to_string
        end
 end
 
-# A `DocCommand` not recognized by documentation tools.
-#
-# Used to provide warnings or any other behavior for unexisting commands.
-class UnknownCommand
-       super AbstractDocCommand
+redef class Text
+       # Read `self` as raw text until `nend` and append it to the `out` buffer.
+       private fun read_until(out: FlatBuffer, start: Int, nend: Char...): Int do
+               var pos = start
+               while pos < length do
+                       var c = self[pos]
+                       var end_reached = false
+                       for n in nend do
+                               if c == n then
+                                       end_reached = true
+                                       break
+                               end
+                       end
+                       if end_reached then break
+                       out.add c
+                       pos += 1
+               end
+               return pos
+       end
 end
 
-# A `DocCommand` that includes the documentation article of a `MEntity`.
+# A command aimed at a documentation tool like `nitdoc` or `nitx`.
 #
-# Syntax: `doc: MEntity::name`.
-class ArticleCommand
-       super AbstractDocCommand
+# `DocCommand` are generally of the form `command: arg | opt1: val1, opt2: val2`.
+abstract class DocCommand
+
+       # Original command string.
+       var string: String
+
+       # Command name.
+       var name: String is noinit
+
+       # Command arguments.
+       var arg: String is noinit, writable
+
+       # Command options.
+       var opts = new HashMap[String, String] is writable
+
+       redef fun to_s do
+               if opts.is_empty then
+                       return "{name}: {arg}"
+               end
+               return "{name}: {arg} | {opts.join(", ", ": ")}"
+       end
 end
 
-# A `DocCommand` that includes the MDoc of a `MEntity`.
+# A `DocCommand` that includes the documentation article of a `MEntity`.
 #
-# Syntax: `comment: MEntity::name`.
+# Syntax: `doc: MEntity::name`.
 class CommentCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "doc"
 end
 
 # A `DocCommand` that includes a list of something.
 #
 # Syntax: `list:kind: <arg>`.
 class ListCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "list"
 end
 
 # A `DocCommand` that includes the list of methods tanking a `MType` as parameter.
 #
 # Syntax: `param: MType`.
 class ParamCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "param"
 end
 
 # A `DocCommand` that includes the list of methods returning a `MType` as parameter.
 #
-# Syntax: `param: MType`.
+# Syntax: `return: MType`.
 class ReturnCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "return"
 end
 
 # A `DocCommand` that includes the list of methods creating new instances of a specific `MType`
 #
 # Syntax: `new: MType`.
 class NewCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "new"
 end
 
 # A `DocCommand` that includes the list of methods calling a specific `MProperty`.
 #
 # Syntax: `call: MEntity::name`.
 class CallCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "call"
 end
 
 # A `DocCommand` that includes the source code of a `MEntity`.
@@ -151,7 +265,9 @@ end
 # * `./src/file.nit` to include source code from a file.
 # * `./src/file.nit:1,2--3,4` to select code between positions.
 class CodeCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "code"
 end
 
 # A `DocCommand` that display an graph for a `MEntity`.
@@ -159,5 +275,7 @@ end
 # Syntax:
 # * `graph: MEntity::name`
 class GraphCommand
-       super AbstractDocCommand
+       super DocCommand
+
+       redef var name = "graph"
 end
index dbb5252..eedfdf6 100644 (file)
@@ -28,7 +28,7 @@ redef class MDoc
        var comment: String is lazy do
                var lines = content.to_a
                if not lines.is_empty then lines.shift
-               return content.join("\n")
+               return lines.join("\n")
        end
 
        # Full comment HTML escaped.
index e8cb281..6a5d045 100644 (file)
@@ -88,13 +88,27 @@ class Nitx
                prompt
        end
 
+       # Parser used to process doc commands
+       var parser: DocCommandParser is lazy do
+               var parser = new DocCommandParser
+               parser.allowed_commands = ["doc", "comment", "list", "param", "return",
+                       "new", "call", "code"]
+               return parser
+       end
+
        # Processes the query string and performs it.
        fun do_query(str: String) do
-               var query = new DocCommand(str)
-               if query isa NitxCommand then
-                       query.execute(self)
+               if str == ":q" then
+                       exit 0
+               else if str == ":h" then
+                       help
                        return
                end
+               var query = parser.parse(str)
+               if query == null then
+                       query = new CommentCommand(str)
+                       query.arg = str
+               end
                var res = query.perform(self, doc)
                var suggest = null
                if res.is_empty then
@@ -105,27 +119,14 @@ class Nitx
        end
 end
 
-redef interface DocCommand
-
-       redef new(query_string) do
-               if query_string == ":q" then
-                       return new NitxQuit
-               else if query_string == ":h" then
-                       return new NitxHelp
-               end
-               var cmd = super(query_string)
-               if cmd isa UnknownCommand then
-                       return new CommentCommand("comment: {query_string}")
-               end
-               return cmd
-       end
+redef class DocCommand
 
        # Looks up the `doc` model and returns possible matches.
        fun perform(nitx: Nitx, doc: DocModel): Array[NitxMatch] is abstract
 
        # Looks up the `doc` model and returns possible suggestions.
        fun suggest(nitx: Nitx, doc: DocModel): nullable Array[MEntity] do
-               return find_suggestions(doc, args.first)
+               return find_suggestions(doc, arg)
        end
 
        # Pretty prints the results for the console.
@@ -184,7 +185,7 @@ class MEntityMatch
 end
 
 redef class CommentCommand
-       redef fun perform(nitx, doc) do return find_mentities(doc, args.first)
+       redef fun perform(nitx, doc) do return find_mentities(doc, arg)
 
        redef fun make_results(nitx, results, suggest) do
                var len = results.length
@@ -207,7 +208,7 @@ end
 redef class ParamCommand
        redef fun perform(nitx, doc) do
                var res = new Array[NitxMatch]
-               var mtype_name = args.first
+               var mtype_name = arg
                for mproperty in doc.mproperties do
                        if not mproperty isa MMethod then continue
                        var msignature = mproperty.intro.msignature
@@ -227,7 +228,7 @@ end
 redef class ReturnCommand
        redef fun perform(nitx, doc) do
                var res = new Array[NitxMatch]
-               var mtype_name = args.first
+               var mtype_name = arg
                for mproperty in doc.mproperties do
                        if not mproperty isa MMethod then continue
                        var msignature = mproperty.intro.msignature
@@ -246,7 +247,7 @@ end
 redef class NewCommand
        redef fun perform(nitx, doc) do
                var res = new Array[NitxMatch]
-               var mtype_name = args.first
+               var mtype_name = arg
                for mpropdef in doc.mpropdefs do
                        var visitor = new TypeInitVisitor(mtype_name)
                        var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
@@ -264,7 +265,7 @@ end
 redef class CallCommand
        redef fun perform(nitx, doc) do
                var res = new Array[NitxMatch]
-               var mprop_name = args.first
+               var mprop_name = arg
                for mpropdef in doc.mpropdefs do
                        var visitor = new MPropertyCallVisitor
                        var npropdef = nitx.ctx.modelbuilder.mpropdef2node(mpropdef)
@@ -279,38 +280,6 @@ redef class CallCommand
        end
 end
 
-# A query to search a Nitdoc documentation page by its name.
-redef class ArticleCommand
-       redef fun perform(nitx, doc) do
-               var res = new Array[NitxMatch]
-               var name = args.first
-               for page in doc.pages.values do
-                       if name == "*" then # FIXME dev only
-                               res.add new PageMatch(self, page)
-                       else if page.title == name then
-                               res.add new PageMatch(self, page)
-                       else if page isa MEntityPage and page.mentity.cs_namespace == name then
-                               res.add new PageMatch(self, page)
-                       end
-               end
-               return res
-       end
-
-       redef fun make_results(nitx, results, suggest) do
-               var len = results.length
-               # FIXME how to render the pager for one worded namespaces like "core"?
-               if len == 1 then
-                       var page = results.first.as(PageMatch).page
-                       var pager = new Pager
-                       pager.add page.write_to_string
-                       pager.render
-                       return page
-               else
-                       return super
-               end
-       end
-end
-
 # A match between a `DocPage` and a `MEntity`.
 class PageMatch
        super NitxMatch
@@ -402,7 +371,7 @@ redef class CodeCommand
        # FIXME refactor this!
        redef fun perform(nitx, doc) do
                var res = new Array[NitxMatch]
-               var name = args.first
+               var name = arg
                # if name is an existing sourcefile, opens it
                if name.file_exists then
                        var fr = new FileReader.open(name)
@@ -442,32 +411,6 @@ class CodeMatch
        redef fun make_list_item do return "* {location}"
 end
 
-
-# A query that contains a nitx command.
-#
-# These commands are prefixed with `:` and are used to control the execution of
-# `nitx` like displaying the help or quiting.
-interface NitxCommand
-       super DocCommand
-
-       # Executes the command.
-       fun execute(nitx: Nitx) is abstract
-end
-
-# Exits nitx.
-class NitxQuit
-       super NitxCommand
-
-       redef fun execute(nitx) do exit 0
-end
-
-# Displays the help message.
-class NitxHelp
-       super NitxCommand
-
-       redef fun execute(nitx) do nitx.help
-end
-
 ## exploration
 
 # Visitor looking for initialized `MType` (new T).
index c026b77..a31f107 100644 (file)
@@ -38,7 +38,8 @@ class ReadmePhase
        fun warning(location: nullable MDLocation, page: ReadmePage, message: String) do
                var loc = null
                if location != null then
-                       loc = location.to_location(page.mentity.mdoc.location.file)
+                       var mdoc = page.mentity.mdoc
+                       if mdoc != null then loc = location.to_location(mdoc.location.file)
                end
                ctx.warning(loc, "readme-warning", message)
        end
@@ -51,14 +52,15 @@ end
 
 redef class ReadmePage
        redef fun build_content(v, doc) do
-               if mentity.mdoc == null then
+               var mdoc = mentity.mdoc
+               if mdoc == null then
                        v.warning(null, self, "Empty README for group `{mentity}`")
                        return
                end
                var proc = new MarkdownProcessor
                proc.emitter = new ReadmeMdEmitter(proc, self, v)
                proc.emitter.decorator = new ReadmeDecorator
-               var md = mentity.mdoc.content.join("\n")
+               var md = mdoc.content.join("\n")
                proc.process(md)
        end
 end
@@ -91,6 +93,7 @@ class ReadmeMdEmitter
        # Called from `add_headline`.
        private fun open_section(lvl: Int, title: String) do
                var section = new ReadmeSection(title.escape_to_c, title, lvl, processor)
+               var current_section = self.current_section
                if current_section == null then
                        page.root.add_child(section)
                else
@@ -119,6 +122,7 @@ class ReadmeMdEmitter
        # This closes the current article, inserts `article` then opens a new article.
        private fun add_article(article: DocArticle) do
                close_article
+               var current_section = self.current_section
                if current_section == null then
                        page.root.add_child(article)
                else
@@ -184,15 +188,18 @@ end
 class ReadmeDecorator
        super MdDecorator
 
+       # Parser used to process doc commands
+       var parser = new DocCommandParser
+
        redef type EMITTER: ReadmeMdEmitter
 
        redef fun add_headline(v, block) do
-               var txt = block.block.first_line.value
+               var txt = block.block.first_line.as(not null).value
                var lvl = block.depth
                if not v.context.is_empty then
                        v.close_article
                        while v.current_section != null do
-                               if v.current_section.depth < lvl then break
+                               if v.current_section.as(not null).depth < lvl then break
                                v.close_section
                        end
                end
@@ -201,9 +208,9 @@ class ReadmeDecorator
        end
 
        redef fun add_wikilink(v, token) do
-               var link = token.link.to_s
-               var cmd = new DocCommand(link)
-               if cmd isa UnknownCommand then
+               var link = token.link.as(not null).to_s
+               var cmd = parser.parse(link)
+               if cmd == null then
                        # search MEntities by name
                        var res = v.find_mentities(link.to_s)
                        # no match, print warning and display wikilink as is
@@ -223,20 +230,21 @@ class ReadmeDecorator
                # TODO real link
                var link = mentity.full_name
                if name == null then name = mentity.name
-               if comment == null and mentity.mdoc != null then
-                       comment = mentity.mdoc.synopsis
+               if comment == null then
+                       var mdoc = mentity.mdoc
+                       if mdoc != null then comment = mdoc.synopsis
                end
                add_link(v, link, name, comment)
        end
 end
 
-redef interface DocCommand
+redef class DocCommand
 
        # Render the content of the doc command.
        fun render(v: ReadmeMdEmitter, token: TokenWikiLink) is abstract
 end
 
-redef class ArticleCommand
+redef class CommentCommand
        redef fun render(v, token) do
                var string = args.first
                var res = v.find_mentities(string)
diff --git a/src/doc/test_doc_commands.nit b/src/doc/test_doc_commands.nit
new file mode 100644 (file)
index 0000000..82f5e7c
--- /dev/null
@@ -0,0 +1,143 @@
+# 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.
+
+module test_doc_commands is test
+
+import doc_commands
+
+class TestDocCommandParser
+       test
+
+       var parser: DocCommandParser
+
+       fun init_parser is before do
+               parser = new DocCommandParser
+       end
+
+       fun test_empty_string is test do
+               var command = parser.parse("")
+               assert command == null
+               assert parser.errors.length == 1
+               assert parser.errors.first.to_s == "Error: empty command name (col: 0)"
+       end
+
+       fun test_bad_string is test do
+               var command = parser.parse(":")
+               assert command == null
+               assert parser.errors.length == 1
+               assert parser.errors.first.to_s == "Error: empty command name (col: 0)"
+       end
+
+       fun test_unknown_command is test do
+               var command = parser.parse("foo: foo")
+               assert command == null
+               assert parser.errors.length == 1
+               assert parser.errors.first.to_s == "Error: unknown command name (col: 0)"
+       end
+
+       fun test_unallowed_command is test do
+               parser.allowed_commands.clear
+               var command = parser.parse("comment: core::Array")
+               assert command == null
+               assert parser.errors.length == 1
+               assert parser.errors.first.to_s == "Error: unknown command name (col: 0)"
+       end
+
+       fun test_no_arg is test do
+               var command = parser.parse("doc:")
+               assert command == null
+               assert parser.errors.length == 1
+               print parser.errors.first
+               assert parser.errors.first.to_s == "Error: empty command arg (col: 4)"
+       end
+
+       fun test_no_opts is test do
+               var command = parser.parse("doc: core::Array")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert parser.errors.is_empty
+       end
+
+       fun test_opts_empty is test do
+               var command = parser.parse("doc: core::Array | ")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert parser.errors.is_empty
+       end
+
+       fun test_1_opt is test do
+               var command = parser.parse("doc: core::Array | opt1: val1 ")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert command.opts.length == 1
+               assert command.opts["opt1"] == "val1"
+               assert parser.errors.is_empty
+       end
+
+       fun test_2_opts is test do
+               var command = parser.parse("doc: core::Array | opt1: val1 , opt2: val2,  ")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert command.opts.length == 2
+               assert command.opts["opt1"] == "val1"
+               assert command.opts["opt2"] == "val2"
+               assert parser.errors.is_empty
+       end
+
+       fun test_empty_opt_name is test do
+               var command = parser.parse("doc: core::Array | opt1: val1  , :")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert command.opts.length == 1
+               assert command.opts["opt1"] == "val1"
+               assert parser.errors.is_empty
+       end
+
+       fun test_empty_opt_value is test do
+               var command = parser.parse("doc: core::Array | opt1:  , opt2: val2,  ")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert command.opts.length == 2
+               assert command.opts["opt1"] == ""
+               assert command.opts["opt2"] == "val2"
+               assert parser.errors.is_empty
+       end
+
+       fun test_empty_opt_value2 is test do
+               var command = parser.parse("doc: core::Array | opt1")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert command.opts.length == 1
+               assert command.opts["opt1"] == ""
+               assert parser.errors.is_empty
+       end
+
+       fun test_empty_opt_value3 is test do
+               var command = parser.parse("doc: core::Array | opt1, opt2: val2")
+               assert command isa CommentCommand
+               assert command.name == "doc"
+               assert command.arg == "core::Array"
+               assert command.opts.length == 2
+               assert command.opts["opt1"] == ""
+               assert command.opts["opt2"] == "val2"
+               assert parser.errors.is_empty
+       end
+end
index 982e206..c116234 100644 (file)
@@ -267,12 +267,12 @@ redef class Catalog
 <div class="sidebar">
 <ul class="box">
 """
-               var tryit = mpackage.metadata("upstream.tryit")
+               var tryit = mpackage.metadata.metadata("upstream.tryit")
                if tryit != null then
                        var e = tryit.html_escape
                        res.add "<li><a href=\"{e}\">Try<span style=\"color:white\">n</span>it!</a></li>\n"
                end
-               var apk = mpackage.metadata("upstream.apk")
+               var apk = mpackage.metadata.metadata("upstream.apk")
                if apk != null then
                        var e = apk.html_escape
                        res.add "<li><a href=\"{e}\">Android apk</a></li>\n"
@@ -280,15 +280,15 @@ redef class Catalog
 
                res.add """</ul>\n<ul class="box">\n"""
 
-               var homepage = mpackage.metadata("upstream.homepage")
+               var homepage = mpackage.metadata.metadata("upstream.homepage")
                if homepage != null then
                        var e = homepage.html_escape
                        res.add "<li><a href=\"{e}\">{e}</a></li>\n"
                end
-               for maintainer in mpackage.maintainers do
+               for maintainer in mpackage.metadata.maintainers do
                        res.add "<li>{maintainer.to_html}</li>"
                end
-               var license = mpackage.metadata("package.license")
+               var license = mpackage.metadata.metadata("package.license")
                if license != null then
                        var e = license.html_escape
                        res.add "<li><a href=\"http://opensource.org/licenses/{e}\">{e}</a> license</li>\n"
@@ -296,22 +296,22 @@ redef class Catalog
                res.add "</ul>\n"
 
                res.add "<h3>Source Code</h3>\n<ul class=\"box\">\n"
-               var browse = mpackage.metadata("upstream.browse")
+               var browse = mpackage.metadata.metadata("upstream.browse")
                if browse != null then
                        var e = browse.html_escape
                        res.add "<li><a href=\"{e}\">{e}</a></li>\n"
                end
-               var git = mpackage.metadata("upstream.git")
+               var git = mpackage.metadata.metadata("upstream.git")
                if git != null then
                        var e = git.html_escape
                        res.add "<li><tt>{e}</tt></li>\n"
                end
-               var last_date = mpackage.last_date
+               var last_date = mpackage.metadata.last_date
                if last_date != null then
                        var e = last_date.html_escape
                        res.add "<li>most recent commit: {e}</li>\n"
                end
-               var first_date = mpackage.first_date
+               var first_date = mpackage.metadata.first_date
                if first_date != null then
                        var e = first_date.html_escape
                        res.add "<li>oldest commit: {e}</li>\n"
@@ -333,7 +333,7 @@ redef class Catalog
 
                res.add "<h3>Tags</h3>\n"
                var ts2 = new Array[String]
-               for t in mpackage.tags do
+               for t in mpackage.metadata.tags do
                        t = t.html_escape
                        ts2.add "<a href=\"../index.html#tag_{t}\">{t}</a>"
                end
@@ -381,7 +381,7 @@ redef class Catalog
                        end
                end
 
-               var contributors = mpackage.contributors
+               var contributors = mpackage.metadata.contributors
                if not contributors.is_empty then
                        res.add "<h3>Contributors</h3>\n<ul class=\"box\">"
                        for c in contributors do
@@ -499,9 +499,9 @@ redef class Catalog
                        res.add "<tr>"
                        res.add "<td><a href=\"p/{p.name}.html\">{p.name}</a></td>"
                        var maint = "?"
-                       if p.maintainers.not_empty then maint = p.maintainers.first.name.html_escape
+                       if p.metadata.maintainers.not_empty then maint = p.metadata.maintainers.first.name.html_escape
                        res.add "<td>{maint}</td>"
-                       res.add "<td>{p.contributors.length}</td>"
+                       res.add "<td>{p.metadata.contributors.length}</td>"
                        if deps.not_empty then
                                res.add "<td>{deps[p].greaters.length-1}</td>"
                                res.add "<td>{deps[p].direct_greaters.length}</td>"
index 678f514..4d4eac9 100644 (file)
@@ -14,7 +14,7 @@
 
 module api_catalog
 
-import web_base
+import api_model
 import catalog
 
 redef class NitwebConfig
@@ -26,126 +26,400 @@ redef class NitwebConfig
        #
        # This method should be called at nitweb startup.
        fun build_catalog do
-               var catalog = new Catalog(modelbuilder)
-               for mpackage in model.mpackages do
-                       catalog.deps.add_node(mpackage)
-                       for mgroup in mpackage.mgroups do
-                               for mmodule in mgroup.mmodules do
-                                       for imported in mmodule.in_importation.direct_greaters do
-                                               var ip = imported.mpackage
-                                               if ip == null or ip == mpackage then continue
-                                               catalog.deps.add_edge(mpackage, ip)
-                                       end
-                               end
-                       end
-                       catalog.git_info(mpackage)
-                       catalog.package_page(mpackage)
-               end
-               self.catalog = catalog
+               self.catalog = new Catalog(modelbuilder)
+               self.catalog.build_catalog(model.mpackages)
        end
 end
 
 redef class APIRouter
        redef init do
                super
-               use("/catalog/highlighted", new APICatalogHighLighted(config))
-               use("/catalog/required", new APICatalogMostRequired(config))
-               use("/catalog/bytags", new APICatalogByTags(config))
-               use("/catalog/contributors", new APICatalogContributors(config))
+               use("/catalog/packages/", new APICatalogPackages(config))
                use("/catalog/stats", new APICatalogStats(config))
+
+               use("/catalog/tags", new APICatalogTags(config))
+               use("/catalog/tag/:tid", new APICatalogTag(config))
+
+               use("/catalog/person/:pid", new APICatalogPerson(config))
+               use("/catalog/person/:pid/maintaining", new APICatalogMaintaining(config))
+               use("/catalog/person/:pid/contributing", new APICatalogContributing(config))
        end
 end
 
 abstract class APICatalogHandler
        super APIHandler
 
-       # List the 10 best packages from `cpt`
-       fun list_best(cpt: Counter[MPackage]): JsonArray do
-               var res = new JsonArray
-               var best = cpt.sort
-               for i in [1..10] do
-                       if i > best.length then break
-                       res.add best[best.length-i]
-               end
-               return res
-       end
+       # Sorter used to sort packages
+       #
+       # Sorting is based on mpackage score.
+       var mpackages_sorter = new CatalogScoreSorter(config.catalog) is lazy
+end
 
-       # List packages by group.
-       fun list_by(map: MultiHashMap[Object, MPackage]): JsonObject do
-               var res = new JsonObject
-               var keys = map.keys.to_a
-               alpha_comparator.sort(keys)
-               for k in keys do
-                       var projs = map[k].to_a
-                       alpha_comparator.sort(projs)
-                       res[k.to_s.html_escape] = new JsonArray.from(projs)
-               end
-               return res
+# Get all the packages from the catalog using pagination
+#
+# `GET /packages?p=1&n=10`: get the list of catalog by page
+class APICatalogPackages
+       super APICatalogHandler
+
+       redef fun get(req, res) do
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var mpackages = config.catalog.mpackages.values.to_a
+               mpackages_sorter.sort(mpackages)
+               var response = new JsonArray.from(mpackages)
+               res.json paginate(response, response.length, page, limit)
        end
 end
 
+# Get the catalog statistics
+#
+# `GET /stats`: return the catalog statistics
 class APICatalogStats
        super APICatalogHandler
 
        redef fun get(req, res) do
-               var obj = new JsonObject
-               obj["packages"] = config.model.mpackages.length
-               obj["maintainers"] = config.catalog.maint2proj.length
-               obj["contributors"] = config.catalog.contrib2proj.length
-               obj["modules"] = config.catalog.mmodules.sum
-               obj["classes"] = config.catalog.mclasses.sum
-               obj["methods"] = config.catalog.mmethods.sum
-               obj["loc"] = config.catalog.loc.sum
-               res.json obj
+               res.json config.catalog.catalog_stats
        end
 end
 
-class APICatalogHighLighted
+# Get all the tags from the catalog
+#
+# `GET /tags`: the list of tags associated with their number of packages
+class APICatalogTags
        super APICatalogHandler
 
-       redef fun get(req, res) do res.json list_best(config.catalog.score)
+       # Sorter to sort tags alphabetically
+       var tags_sorter = new CatalogTagsSorter
+
+       redef fun get(req, res) do
+               var obj = new JsonObject
+
+               var tags = config.catalog.tag2proj.keys.to_a
+               tags_sorter.sort(tags)
+
+               for tag in tags do
+                       if not config.catalog.tag2proj.has_key(tag) then continue
+                       obj[tag] = config.catalog.tag2proj[tag].length
+               end
+               res.json obj
+       end
 end
 
-class APICatalogMostRequired
+# Get the packages related to a tag
+#
+# `GET /tag/:tid?p=1&n=10`: return a paginated list of packages
+class APICatalogTag
        super APICatalogHandler
 
        redef fun get(req, res) do
-               if config.catalog.deps.not_empty then
-                       var reqs = new Counter[MPackage]
-                       for p in config.model.mpackages do
-                               reqs[p] = config.catalog.deps[p].smallers.length - 1
-                       end
-                       res.json list_best(reqs)
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var id = req.param("tid")
+               if id == null then
+                       res.api_error(400, "Missing tag")
+                       return
+               end
+               id = id.from_percent_encoding
+               if not config.catalog.tag2proj.has_key(id) then
+                       res.api_error(404, "Tag not found")
                        return
                end
-               res.json new JsonArray
+               var obj = new JsonObject
+               obj["tag"] = id
+               var mpackages = config.catalog.tag2proj[id]
+               mpackages_sorter.sort(mpackages)
+               var response = new JsonArray.from(mpackages)
+               obj["packages"] = paginate(response, response.length, page, limit)
+               res.json obj
        end
 end
 
-class APICatalogByTags
+# Get a person existing in the catalog
+#
+# `GET /person/:pid`: get the person with `pid`
+class APICatalogPerson
        super APICatalogHandler
 
-       redef fun get(req, res) do res.json list_by(config.catalog.tag2proj)
+       # Get the person with `:pid` or throw a 404 error
+       fun get_person(req: HttpRequest, res: HttpResponse): nullable Person do
+               var id = req.param("pid")
+               if id == null then
+                       res.api_error(400, "Missing package full_name")
+                       return null
+               end
+               id = id.from_percent_encoding
+               if not config.catalog.name2person.has_key(id) then
+                       res.api_error(404, "Person not found")
+                       return null
+               end
+               return config.catalog.name2person[id]
+       end
+
+       redef fun get(req, res) do
+               var person = get_person(req, res)
+               if person == null then return
+               res.json person
+       end
 end
 
-class APICatalogContributors
-       super APICatalogHandler
+# Get the list of mpackages maintained by a person
+#
+# `GET /person/:pid/maintaining?p=1&n=10`: return a paginated list of packages
+class APICatalogMaintaining
+       super APICatalogPerson
 
        redef fun get(req, res) do
-               var obj = new JsonObject
-               obj["maintainers"] = new JsonArray.from(config.catalog.maint2proj.keys)
-               obj["contributors"] = new JsonArray.from(config.catalog.contrib2proj.keys)
-               res.json obj
+               var person = get_person(req, res)
+               if person == null then return
+
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var array = new Array[MPackage]
+               if config.catalog.maint2proj.has_key(person) then
+                       array = config.catalog.maint2proj[person].to_a
+               end
+               mpackages_sorter.sort(array)
+               var response = new JsonArray.from(array)
+               res.json paginate(response, response.length, page, limit)
+       end
+end
+
+# Get the list of mpackages contributed by a person
+#
+# `GET /person/:pid/contributing?p=1&n=10`: return a paginated list of packages
+class APICatalogContributing
+       super APICatalogPerson
+
+       redef fun get(req, res) do
+               var person = get_person(req, res)
+               if person == null then return
+
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var array = new Array[MPackage]
+               if config.catalog.contrib2proj.has_key(person) then
+                       array = config.catalog.contrib2proj[person].to_a
+               end
+               mpackages_sorter.sort(array)
+               var response = new JsonArray.from(array)
+               res.json paginate(response, response.length, page, limit)
+       end
+end
+
+redef class APIEntity
+       redef fun get(req, res) do
+               var mentity = mentity_from_uri(req, res)
+               if mentity == null then return
+
+               # Special case for packages (catalog view)
+               if mentity isa MPackage then
+                       res.raw_json mentity.to_full_catalog_json(plain=true, config.catalog)
+               else
+                       res.raw_json mentity.to_full_json
+               end
+       end
+end
+
+redef class APISearch
+       super APICatalogHandler
+
+       redef fun search(query, limit) do
+               var index = config.view.index
+
+               # lookup by name prefix
+               var matches = index.find_by_name_prefix(query).uniq.
+                       sort(lname_sorter, name_sorter, kind_sorter)
+               matches = matches.rerank.sort(vis_sorter, score_sorter)
+
+               # lookup by tags
+               var malus = matches.length
+               if config.catalog.tag2proj.has_key(query) then
+                       for mpackage in config.catalog.tag2proj[query] do
+                               matches.add new IndexMatch(mpackage, malus)
+                               malus += 1
+                       end
+                       matches = matches.uniq.rerank.sort(vis_sorter, score_sorter)
+               end
+
+               # lookup by full_name prefix
+               malus = matches.length
+               var full_matches = new IndexMatches
+               for match in index.find_by_full_name_prefix(query).
+                       sort(lfname_sorter, fname_sorter) do
+                       match.score += 1
+                       full_matches.add match
+               end
+               matches = matches.uniq
+
+               # lookup by similarity
+               malus = matches.length
+               var sim_matches = new IndexMatches
+               for match in index.find_by_similarity(query).sort(score_sorter, lname_sorter, name_sorter) do
+                       if match.score > query.length then break
+                       match.score += 1
+                       sim_matches.add match
+               end
+               matches.add_all sim_matches
+               matches = matches.uniq
+               return matches.rerank.sort(vis_sorter, score_sorter).mentities
+       end
+
+       private var score_sorter = new ScoreComparator
+       private var vis_sorter = new VisibilityComparator
+       private var name_sorter = new NameComparator
+       private var lname_sorter = new NameLengthComparator
+       private var fname_sorter = new FullNameComparator
+       private var lfname_sorter = new FullNameLengthComparator
+       private var kind_sorter = new MEntityComparator
+end
+
+redef class Catalog
+
+       # Build the catalog from `mpackages`
+       fun build_catalog(mpackages: Array[MPackage]) do
+               # Compute the poset
+               for p in mpackages do
+                       var g = p.root
+                       assert g != null
+                       modelbuilder.scan_group(g)
+
+                       deps.add_node(p)
+                       for gg in p.mgroups do for m in gg.mmodules do
+                               for im in m.in_importation.direct_greaters do
+                                       var ip = im.mpackage
+                                       if ip == null or ip == p then continue
+                                       deps.add_edge(p, ip)
+                               end
+                       end
+               end
+               # Build the catalog
+               for mpackage in mpackages do
+                       package_page(mpackage)
+                       git_info(mpackage)
+                       mpackage_stats(mpackage)
+               end
+       end
+end
+
+redef class MPackageMetadata
+       serialize
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("license", license)
+               v.serialize_attribute("maintainers", maintainers)
+               v.serialize_attribute("contributors", contributors)
+               v.serialize_attribute("tags", tags)
+               v.serialize_attribute("tryit", tryit)
+               v.serialize_attribute("apk", apk)
+               v.serialize_attribute("homepage", homepage)
+               v.serialize_attribute("browse", browse)
+               v.serialize_attribute("git", git)
+               v.serialize_attribute("issues", issues)
+               v.serialize_attribute("first_date", first_date)
+               v.serialize_attribute("last_date", last_date)
+       end
+end
+
+# Catalog statistics
+redef class CatalogStats
+       serialize
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("packages", packages)
+               v.serialize_attribute("maintainers", maintainers)
+               v.serialize_attribute("contributors", contributors)
+               v.serialize_attribute("tags", tags)
+               v.serialize_attribute("modules", modules)
+               v.serialize_attribute("classes", classes)
+               v.serialize_attribute("methods", methods)
+               v.serialize_attribute("loc", loc)
+       end
+end
+
+# MPackage statistics for the catalog
+redef class MPackageStats
+       serialize
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("mmodules", mmodules)
+               v.serialize_attribute("mclasses", mclasses)
+               v.serialize_attribute("mmethods", mmethods)
+               v.serialize_attribute("loc", loc)
+               v.serialize_attribute("errors", errors)
+               v.serialize_attribute("warnings", warnings)
+               v.serialize_attribute("warnings_per_kloc", warnings_per_kloc)
+               v.serialize_attribute("documentation_score", documentation_score)
+               v.serialize_attribute("commits", commits)
+               v.serialize_attribute("score", score)
        end
 end
 
 redef class Person
-       super Serializable
+       serialize
 
        redef fun core_serialize_to(v) do
+               super
                v.serialize_attribute("name", name)
                v.serialize_attribute("email", email)
-               v.serialize_attribute("page", page)
-               v.serialize_attribute("hash", (email or else "").md5.to_lower)
+               v.serialize_attribute("gravatar", gravatar)
+       end
+end
+
+redef class MPackage
+       # Serialize the full catalog version of `self` to JSON
+       #
+       # See: `FullCatalogSerializer`
+       fun to_full_catalog_json(catalog: Catalog, plain, pretty: nullable Bool): String do
+               var stream = new StringWriter
+               var serializer = new FullCatalogSerializer(stream, catalog)
+               serializer.plain_json = plain or else false
+               serializer.pretty_json = pretty or else false
+               serializer.serialize self
+               stream.close
+               return stream.to_s
+       end
+
+       redef fun core_serialize_to(v) do
+               super
+               v.serialize_attribute("metadata", metadata)
+               if v isa FullCatalogSerializer then
+                       v.serialize_attribute("stats", v.catalog.mpackages_stats[self])
+
+                       var parents = v.catalog.deps[self].direct_greaters.to_a
+                       v.serialize_attribute("dependencies", v.deps_to_json(parents))
+                       var children = v.catalog.deps[self].direct_smallers.to_a
+                       v.serialize_attribute("clients", v.deps_to_json(children))
+               end
+       end
+end
+
+# CatalogSerializer decorate the Package JSON with full catalog metadata
+#
+# See MEntity::to_full_catalog_json.
+class FullCatalogSerializer
+       super FullJsonSerializer
+
+       # Catalog used to decorate the MPackages
+       var catalog: Catalog
+
+       private fun deps_to_json(mpackages: Array[MPackage]): JsonArray do
+               var res = new JsonArray
+               for mpackage in mpackages do
+                       res.add dep_to_json(mpackage)
+               end
+               return res
+       end
+
+       private fun dep_to_json(mpackage: MPackage): JsonObject do
+               var obj = new JsonObject
+               obj["name"] = mpackage.name
+               var mdoc = mpackage.mdoc_or_fallback
+               if mdoc != null then
+                       obj["synopsis"] = mdoc.synopsis.write_to_string
+               end
+               return obj
        end
 end
index bbdc9db..a5ca012 100644 (file)
@@ -87,10 +87,7 @@ class NitwebDecorator
        end
 
        redef fun add_wikilink(v, token) do
-               var link = token.link
-               if link == null then return
-               var cmd = new DocCommand(link.write_to_string)
-               cmd.render(v, token, view)
+               v.render_wikilink(token, view)
        end
 end
 
@@ -105,20 +102,51 @@ class NitwebInlineDecorator
        var modelbuilder: ModelBuilder
 
        redef fun add_wikilink(v, token) do
-               var link = token.link
-               if link == null then return
-               var cmd = new DocCommand(link.write_to_string)
-               cmd.render(v, token, view)
+               v.render_wikilink(token, view)
        end
 end
 
 redef class MarkdownEmitter
+
+       # Parser used to process doc commands
+       var parser = new DocCommandParser
+
+       # Render a wikilink
+       fun render_wikilink(token: TokenWikiLink, model: ModelView) do
+               var link = token.link
+               if link == null then return
+               var name = token.name
+               if name != null then link = "{name} | {link}"
+               var cmd = parser.parse(link.write_to_string)
+               if cmd == null then
+                       var full_name = if token.link != null then token.link.as(not null).write_to_string.trim else null
+                       if full_name == null or full_name.is_empty then
+                               write_error("empty wikilink")
+                               return
+                       end
+                       var mentity = find_mentity(model, full_name)
+                       if mentity == null then return
+                       name = if token.name != null then token.name.as(not null).to_s else null
+                       write_mentity_link(mentity, name)
+                       return
+               else
+                       for message in parser.errors do
+                               if message.level == 1 then
+                                       write_error(message.message)
+                               else if message.level > 1 then
+                                       write_warning(message.message)
+                               end
+                       end
+               end
+               cmd.render(self, token, model)
+       end
+
        # Find the MEntity that matches `name`.
        #
        # Write an error if the entity is not found
        fun find_mentity(model: ModelView, name: nullable String): nullable MEntity do
                if name == null then
-                       write_error("No MEntity found")
+                       write_error("no MEntity found")
                        return null
                end
                # Lookup by full name
@@ -129,7 +157,7 @@ redef class MarkdownEmitter
                if mentities.is_empty then
                        var suggest = model.find(name, 3)
                        var msg = new Buffer
-                       msg.append "No MEntity found for name `{name}`"
+                       msg.append "no MEntity found for name `{name}`"
                        if suggest.not_empty then
                                msg.append " (suggestions: "
                                var i = 0
@@ -144,7 +172,7 @@ redef class MarkdownEmitter
                        return null
                else if mentities.length > 1 then
                        var msg = new Buffer
-                       msg.append "Conflicts for name `{name}`"
+                       msg.append "conflicts for name `{name}`"
                        msg.append " (conflicts: "
                        var i = 0
                        for s in mentities do
@@ -195,76 +223,44 @@ redef class MarkdownEmitter
        end
 end
 
-redef interface DocCommand
+redef class DocCommand
 
        # Emit the HTML related to the execution of this doc command
        fun render(v: MarkdownEmitter, token: TokenWikiLink, model: ModelView) do
-               v.write_error("Not yet implemented command `{token.link or else "null"}`")
-       end
-end
-
-redef class UnknownCommand
-       redef fun render(v, token, model) do
-               var link = token.link
-               if link == null then
-                       v.write_error("Empty command")
-                       return
-               end
-               var full_name = link.write_to_string
-               var mentity = v.find_mentity(model, full_name)
-               if mentity == null then return
-               v.write_mentity_link(mentity)
+               v.write_error("not yet implemented command `{token.link or else "null"}`")
        end
 end
 
-redef class ArticleCommand
+redef class CommentCommand
        redef fun render(v, token, model) do
-               if args.is_empty then
-                       v.write_error("Expected one arg: the MEntity name")
-                       return
-               end
-               var name = args.first
+               var name = arg
                var mentity = v.find_mentity(model, name)
                if mentity == null then return
                var mdoc = mentity.mdoc_or_fallback
                if mdoc == null then
-                       v.write_warning("No MDoc for mentity `{name}`")
+                       v.write_warning("no MDoc for mentity `{name}`")
                        return
                end
                v.add "<h3>"
-               v.write_mentity_link(mentity)
-               v.add " - "
-               v.emit_text mdoc.synopsis
-               v.add "</h3>"
-               v.add v.processor.process(mdoc.comment).write_to_string
-       end
-end
-
-redef class CommentCommand
-       redef fun render(v, token, model) do
-               if args.is_empty then
-                       v.write_error("Expected one arg: the MEntity name")
-                       return
+               if not opts.has_key("no-link") then
+                       v.write_mentity_link(mentity)
                end
-               var name = args.first
-               var mentity = v.find_mentity(model, name)
-               if mentity == null then return
-               var mdoc = mentity.mdoc_or_fallback
-               if mdoc == null then
-                       v.write_warning("No MDoc for mentity `{name}`")
-                       return
+               if not opts.has_key("no-link") and not opts.has_key("no-synopsis") then
+                       v.add " - "
+               end
+               if not opts.has_key("no-synopsis") then
+                       v.emit_text mdoc.html_synopsis.write_to_string
+               end
+               v.add "</h3>"
+               if not opts.has_key("no-comment") then
+                       v.add v.processor.process(mdoc.comment).write_to_string
                end
-               v.add v.processor.process(mdoc.comment).write_to_string
        end
 end
 
 redef class ListCommand
        redef fun render(v, token, model) do
-               if args.is_empty then
-                       v.write_error("Expected one arg: the MEntity name")
-                       return
-               end
-               var name = args.first
+               var name = arg
                var mentity = v.find_mentity(model, name)
                if mentity == null then return
                if mentity isa MPackage then
@@ -283,25 +279,21 @@ redef class ListCommand
                else if mentity isa MProperty then
                        v.write_mentity_list(mentity.mpropdefs)
                else
-                       v.write_error("No list found for name `{name}`")
+                       v.write_error("no list found for name `{name}`")
                end
        end
 end
 
 redef class CodeCommand
        redef fun render(v, token, model) do
-               if args.is_empty then
-                       v.write_error("Expected one arg: the MEntity name")
-                       return
-               end
-               var name = args.first
+               var name = arg
                var mentity = v.find_mentity(model, name)
                if mentity == null then return
                if mentity isa MClass then mentity = mentity.intro
                if mentity isa MProperty then mentity = mentity.intro
                var source = render_source(mentity, v.decorator.as(NitwebDecorator).modelbuilder)
                if source == null then
-                       v.write_error("No source for MEntity `{name}`")
+                       v.write_error("no source for MEntity `{name}`")
                        return
                end
                v.add "<pre>"
@@ -321,15 +313,15 @@ end
 
 redef class GraphCommand
        redef fun render(v, token, model) do
-               if args.is_empty then
-                       v.write_error("Expected one arg: the MEntity name")
-                       return
-               end
-               var name = args.first
+               var name = arg
                var mentity = v.find_mentity(model, name)
                if mentity == null then return
                var g = new InheritanceGraph(mentity, model)
-               v.add g.draw(3, 3).to_svg
+               var pdepth = if opts.has_key("pdepth") and opts["pdepth"].is_int then
+                       opts["pdepth"].to_i else 3
+               var cdepth = if opts.has_key("cdepth") and opts["cdepth"].is_int then
+                       opts["cdepth"].to_i else 3
+               v.add g.draw(pdepth, cdepth).to_svg
        end
 end
 
index 4f1a148..328eff9 100644 (file)
@@ -104,13 +104,19 @@ class APISearch
        super APIList
 
        redef fun get(req, res) do
-               var q = req.string_arg("q")
-               if q == null then
-                       res.json new JsonArray
+               var query = req.string_arg("q")
+               if query == null then
+                       res.api_error(400, "Missing search string")
                        return
                end
-               var n = req.int_arg("n")
-               res.json new JsonArray.from(config.view.find(q, n))
+               var page = req.int_arg("p")
+               var limit = req.int_arg("n")
+               var response = new JsonArray.from(search(query, limit))
+               res.json paginate(response, response.length, page, limit)
+       end
+
+       fun search(query: String, limit: nullable Int): Array[MEntity] do
+               return config.view.find(query)
        end
 end
 
index d50cc2a..ac63f53 100644 (file)
@@ -82,6 +82,46 @@ abstract class APIHandler
                end
                return mentity
        end
+
+       # Paginate a json array
+       #
+       # Returns only a subset of `results` depending on the current `page` and the
+       # number of elements to return set by `limit`.
+       #
+       # Transforms the json array into an object:
+       # ~~~json
+       # {
+       #       "page": 2,
+       #       "limit": 10,
+       #       "results: [ ... ],
+       #       "max": 5,
+       #       "total": 49
+       # }
+       # ~~~
+       fun paginate(results: JsonArray, count: Int, page, limit: nullable Int): JsonObject do
+               if page == null or page <= 0 then page = 1
+               if limit == null or limit <= 0 then limit = 20
+
+               var max = count / limit
+               if max == 0 then
+                       page = 1
+                       max = 1
+               else if page > max then
+                       page = max
+               end
+
+               var lstart = (page - 1) * limit
+               var lend = limit
+               if lstart + lend > count then lend = count - lstart
+
+               var res = new JsonObject
+               res["page"] = page
+               res["limit"] = limit
+               res["results"] = new JsonArray.from(results.subarray(lstart, lend))
+               res["max"] = max
+               res["total"] = count
+               return res
+       end
 end
 
 # A Rooter dedicated to APIHandlers.