+++ /dev/null
-<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&default=retro">
- {{contributor.name}}
- </li>
- </ul>
-</div>
<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&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>
+++ /dev/null
-<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>
+++ /dev/null
-<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>
--- /dev/null
+<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>«</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>»</span></a>
+ </li>
+ </ul>
+</nav>
<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>
</div>
</div>
<div class='col-xs-7'>
- <search-field />
+ <ui-search-field />
</div>
<div class='col-xs-2'>
<user-menu />
<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>
--- /dev/null
+/*
+ * 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});
+ })
+ })
+})();
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'
};
}])
.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',
+++ /dev/null
-/*
- * 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'
- };
- })
-})();
*/
(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
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'
+ };
+ })
})();
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 {
}
.card-list > .card:first-child {
- border-top: 1px solid #ccc;
+ border-top: 1px solid #ddd;
}
.card-list > .card {
background: #FF8100;
}
+[ng-click] {
+ cursor: pointer;
+}
+
/* Body */
body {
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; }
.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;
+}
+++ /dev/null
-<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>
+++ /dev/null
-<div>
- <entity-list list-title='Highlighted packages'
- list-entities='vm.highlighted'
- list-object-filter='{}' />
-</div>
-<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> <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> <span>{{key}}</span>
+
+ </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>
+++ /dev/null
-<div>
- <entity-list list-title='Most required'
- list-entities='vm.required'
- list-object-filter='{}' />
-</div>
--- /dev/null
+<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&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>
--- /dev/null
+<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>
<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>
<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 />
--- /dev/null
+<div class='card'>
+ <div class='card-body'>
+ <entity-rating mentity='vm.mentity' ratings='vm.ratings'>
+ </div>
+</div>
--- /dev/null
+<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>
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
# 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
# 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
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&default=retro\"> "
+ var gravatar = self.gravatar
+ if gravatar != null then
+ res += "<img src=\"https://secure.gravatar.com/avatar/{gravatar}?size=20&default=retro\"> "
end
res += e
if page != null then res += "</a>"
# 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]
# 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
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
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
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
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
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
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)
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
# * `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`.
# * `./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`.
# Syntax:
# * `graph: MEntity::name`
class GraphCommand
- super AbstractDocCommand
+ super DocCommand
+
+ redef var name = "graph"
end
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.
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
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.
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
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
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
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)
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)
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
# 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)
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).
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
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
# 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
# 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
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
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
# 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)
--- /dev/null
+# 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
<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"
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"
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"
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
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
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>"
module api_catalog
-import web_base
+import api_model
import catalog
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
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
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
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
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
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
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>"
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
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
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.