فهرست منبع

Add README and stars + other facts from github

Jordi Boggiano 10 سال پیش
والد
کامیت
7905c701bb

+ 4 - 2
composer.json

@@ -23,7 +23,7 @@
         "psr-0": { "Packagist": "src/" }
     },
     "require": {
-        "php": ">=5.3.3",
+        "php": ">=5.5",
         "symfony/symfony": "2.3.*@dev",
         "doctrine/orm": "~2.3",
         "doctrine/doctrine-bundle": "1.2.*",
@@ -52,7 +52,9 @@
 
         "kriswallsmith/assetic": "~1.2",
         "pagerfanta/pagerfanta": "~1.0",
-        "knplabs/knp-menu-bundle": "^2.0"
+        "knplabs/knp-menu-bundle": "^2.0",
+        "ezyang/htmlpurifier": "^4.6"
+
     },
     "scripts": {
         "post-install-cmd": [

+ 49 - 5
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "2259146deb50eb0cc9a96e86253ab4d5",
+    "hash": "abcc8fe5bfd89428e322fea1635a6ce5",
     "packages": [
         {
             "name": "composer/composer",
@@ -678,6 +678,50 @@
             ],
             "time": "2014-12-16 13:45:01"
         },
+        {
+            "name": "ezyang/htmlpurifier",
+            "version": "v4.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ezyang/htmlpurifier.git",
+                "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/6f389f0f25b90d0b495308efcfa073981177f0fd",
+                "reference": "6f389f0f25b90d0b495308efcfa073981177f0fd",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "HTMLPurifier": "library/"
+                },
+                "files": [
+                    "library/HTMLPurifier.composer.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL"
+            ],
+            "authors": [
+                {
+                    "name": "Edward Z. Yang",
+                    "email": "admin@htmlpurifier.org",
+                    "homepage": "http://ezyang.com"
+                }
+            ],
+            "description": "Standards compliant HTML filter written in PHP",
+            "homepage": "http://htmlpurifier.org/",
+            "keywords": [
+                "html"
+            ],
+            "time": "2013-11-30 08:25:19"
+        },
         {
             "name": "friendsofsymfony/user-bundle",
             "version": "v1.3.5",
@@ -756,7 +800,7 @@
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/hwi/HWIOAuthBundle/zipball/2e12c7ab9cf83ce0d3e9d6a98315bbf21ab23ab3",
+                "url": "https://api.github.com/repos/hwi/HWIOAuthBundle/zipball/d7caff8257d7b5807f2f56d03d7bddca6484265b",
                 "reference": "2e12c7ab9cf83ce0d3e9d6a98315bbf21ab23ab3",
                 "shasum": ""
             },
@@ -2445,7 +2489,7 @@
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/symfony/zipball/cff69aa18b5db9aac978b35f186ff670ff75d3e9",
+                "url": "https://api.github.com/repos/symfony/symfony/zipball/11010ca691aee993fd3c7198faadb22923201a50",
                 "reference": "cff69aa18b5db9aac978b35f186ff670ff75d3e9",
                 "shasum": ""
             },
@@ -2716,7 +2760,7 @@
             },
             "dist": {
                 "type": "zip",
-                "url": "https://packages.zendframework.com/composer/zendframework-zendframework-57de1d9e3fe0564d2572e0a961e905ae2279c003-zip-992dda.zip",
+                "url": "https://api.github.com/repos/zendframework/zf2/zipball/57de1d9e3fe0564d2572e0a961e905ae2279c003",
                 "reference": "2.0.8",
                 "shasum": "0b8790b1e9f5cbb5b50c0443851b26e1ed36e80c"
             },
@@ -2831,7 +2875,7 @@
     "prefer-stable": false,
     "prefer-lowest": false,
     "platform": {
-        "php": ">=5.3.3"
+        "php": ">=5.5"
     },
     "platform-dev": []
 }

+ 1 - 0
doc/schema.xml

@@ -239,6 +239,7 @@
    <field name="trendiness" type="float" indexed="true" stored="true" />
    <field name="downloads" type="int" indexed="true" stored="true" />
    <field name="favers" type="int" indexed="true" stored="true" />
+   <field name="language" type="string" indexed="true" stored="true" />
    <field name="repository" type="string" indexed="false" stored="true" />
    <field name="abandoned" type="int" indexed="false" stored="true" />
    <field name="replacementPackage" type="string" indexed="false" stored="true" />

+ 2 - 2
src/Packagist/WebBundle/Command/CompileStatsCommand.php

@@ -119,7 +119,7 @@ class CompileStatsCommand extends ContainerAwareCommand
             $ids[] = $row['id'];
         }
 
-        // add downloads from the last 5 days to the solr index
+        // add downloads from the last 7 days to the solr index
         $solarium = $this->getContainer()->get('solarium.client');
 
         if ($verbose) {
@@ -127,7 +127,7 @@ class CompileStatsCommand extends ContainerAwareCommand
         }
 
         while ($id = array_shift($ids)) {
-            $trendiness = $this->sumLastNDays(5, $id, $yesterday);
+            $trendiness = $this->sumLastNDays(7, $id, $yesterday);
 
             $redis->zadd('downloads:trending:new', $trendiness, $id);
             $redis->zadd('downloads:absolute:new', $redis->get('dl:'.$id), $id);

+ 1 - 0
src/Packagist/WebBundle/Command/IndexPackagesCommand.php

@@ -183,6 +183,7 @@ class IndexPackagesCommand extends ContainerAwareCommand
         $document->setField('downloads', $downloadManager->getTotalDownloads($package));
         $document->setField('favers', $favoriteManager->getFaverCount($package));
         $document->setField('repository', $package->getRepository());
+        $document->setField('language', $package->getLanguage());
         if ($package->isAbandoned()) {
             $document->setField('abandoned', 1);
             $document->setField('replacementPackage', $package->getReplacementPackage() ?: '');

+ 1 - 1
src/Packagist/WebBundle/Command/UpdatePackagesCommand.php

@@ -108,7 +108,7 @@ class UpdatePackagesCommand extends ContainerAwareCommand
                     }
                     $repository = new VcsRepository(array('url' => $package->getRepository()), $io, $config);
                     $repository->setLoader($loader);
-                    $updater->update($package, $repository, $flags, $start);
+                    $updater->update($io, $config, $package, $repository, $flags, $start);
                 } catch (InvalidRepositoryException $e) {
                     $output->writeln('<error>Broken repository in '.$router->generate('view_package', array('name' => $package->getName()), true).': '.$e->getMessage().'</error>');
                     if ($input->getOption('notify-failures')) {

+ 4 - 4
src/Packagist/WebBundle/Controller/ApiController.php

@@ -207,14 +207,14 @@ class ApiController extends Controller
         // put both updating the database and scanning the repository in a transaction
         $em = $this->get('doctrine.orm.entity_manager');
         $updater = $this->get('packagist.package_updater');
+        $config = Factory::createConfig();
         $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE);
+        $io->loadConfiguration($config);
 
         try {
             foreach ($packages as $package) {
-                $em->transactional(function($em) use ($package, $updater, $io) {
+                $em->transactional(function($em) use ($package, $updater, $io, $config) {
                     // prepare dependencies
-                    $config = Factory::createConfig();
-                    $io->loadConfiguration($config);
                     $loader = new ValidatingArrayLoader(new ArrayLoader());
 
                     // prepare repository
@@ -222,7 +222,7 @@ class ApiController extends Controller
                     $repository->setLoader($loader);
 
                     // perform the actual update (fetch and re-scan the repository's source)
-                    $updater->update($package, $repository);
+                    $updater->update($io, $config, $package, $repository);
 
                     // update the package entity
                     $package->setAutoUpdated(true);

+ 23 - 5
src/Packagist/WebBundle/Controller/Controller.php

@@ -21,20 +21,38 @@ class Controller extends BaseController
 {
     protected function getPackagesMetadata($packages)
     {
+        $favMgr = $this->get('packagist.favorite_manager');
+        $dlMgr = $this->get('packagist.download_manager');
+
         try {
             $ids = array();
 
+            if (!count($packages)) {
+                return;
+            }
+
+            $favs = array();
+            $solarium = false;
             foreach ($packages as $package) {
-                $ids[] = $package instanceof \Solarium_Document_ReadOnly ? $package->id : $package->getId();
+                if ($package instanceof \Solarium_Document_ReadOnly) {
+                    $solarium = true;
+                    $ids[] = $package->id;
+                } else {
+                    $ids[] = $package->getId();
+                    $favs[$package->getId()] = $favMgr->getFaverCount($package);
+                }
             }
 
-            if (!$ids) {
-                return;
+            if ($solarium) {
+                return array(
+                    'downloads' => $dlMgr->getPackagesDownloads($ids),
+                    'favers' => $favMgr->getFaverCounts($ids),
+                );
             }
 
             return array(
-                'downloads' => $this->get('packagist.download_manager')->getPackagesDownloads($ids),
-                'favers' => $this->get('packagist.favorite_manager')->getFaverCounts($ids),
+                'downloads' => $dlMgr->getPackagesDownloads($ids),
+                'favers' => $favs,
             );
         } catch (\Predis\Connection\ConnectionException $e) {}
     }

+ 1 - 1
src/Packagist/WebBundle/Controller/WebController.php

@@ -890,7 +890,7 @@ class WebController extends Controller
                 $repository->setLoader($loader);
 
                 try {
-                    $updater->update($package, $repository, $updateEqualRefs ? Updater::UPDATE_EQUAL_REFS : 0);
+                    $updater->update($io, $config, $package, $repository, $updateEqualRefs ? Updater::UPDATE_EQUAL_REFS : 0);
                 } catch (\Exception $e) {
                     return new Response(json_encode(array(
                         'status' => 'error',

+ 134 - 0
src/Packagist/WebBundle/Entity/Package.php

@@ -62,6 +62,36 @@ class Package
      */
     private $description;
 
+    /**
+     * @ORM\Column(type="string", nullable=true)
+     */
+    private $language;
+
+    /**
+     * @ORM\Column(type="text", nullable=true)
+     */
+    private $readme;
+
+    /**
+     * @ORM\Column(type="integer", nullable=true, name="github_stars")
+     */
+    private $gitHubStars;
+
+    /**
+     * @ORM\Column(type="integer", nullable=true, name="github_watches")
+     */
+    private $gitHubWatches;
+
+    /**
+     * @ORM\Column(type="integer", nullable=true, name="github_forks")
+     */
+    private $gitHubForks;
+
+    /**
+     * @ORM\Column(type="integer", nullable=true, name="github_open_issues")
+     */
+    private $gitHubOpenIssues;
+
     /**
      * @ORM\OneToMany(targetEntity="Packagist\WebBundle\Entity\Version", mappedBy="package")
      */
@@ -336,6 +366,110 @@ class Package
         return $this->description;
     }
 
+    /**
+     * Set language
+     *
+     * @param string $language
+     */
+    public function setLanguage($language)
+    {
+        $this->language = $language;
+    }
+
+    /**
+     * Get language
+     *
+     * @return string
+     */
+    public function getLanguage()
+    {
+        return $this->language;
+    }
+
+    /**
+     * Set readme
+     *
+     * @param string $readme
+     */
+    public function setReadme($readme)
+    {
+        $this->readme = $readme;
+    }
+
+    /**
+     * Get readme
+     *
+     * @return string
+     */
+    public function getReadme()
+    {
+        return $this->readme;
+    }
+
+    /**
+     * @param int $val
+     */
+    public function setGitHubStars($val)
+    {
+        $this->gitHubStars = $val;
+    }
+
+    /**
+     * @return int
+     */
+    public function getGitHubStars()
+    {
+        return $this->gitHubStars;
+    }
+
+    /**
+     * @param int $val
+     */
+    public function setGitHubWatches($val)
+    {
+        $this->gitHubWatches = $val;
+    }
+
+    /**
+     * @return int
+     */
+    public function getGitHubWatches()
+    {
+        return $this->gitHubWatches;
+    }
+
+    /**
+     * @param int $val
+     */
+    public function setGitHubForks($val)
+    {
+        $this->gitHubForks = $val;
+    }
+
+    /**
+     * @return int
+     */
+    public function getGitHubForks()
+    {
+        return $this->gitHubForks;
+    }
+
+    /**
+     * @param int $val
+     */
+    public function setGitHubOpenIssues($val)
+    {
+        $this->gitHubOpenIssues = $val;
+    }
+
+    /**
+     * @return int
+     */
+    public function getGitHubOpenIssues()
+    {
+        return $this->gitHubOpenIssues;
+    }
+
     /**
      * Set createdAt
      *

+ 11 - 0
src/Packagist/WebBundle/Entity/PackageRepository.php

@@ -258,6 +258,17 @@ class PackageRepository extends EntityRepository
         return $qb->getQuery()->getResult();
     }
 
+    public function getGitHubStars(array $ids)
+    {
+        $qb = $this->getEntityManager()->createQueryBuilder();
+        $qb->select('p.gitHubStars', 'p.id')
+            ->from('Packagist\WebBundle\Entity\Package', 'p')
+            ->where($qb->expr()->in('p.id', ':ids'))
+            ->setParameter('ids', $ids);
+
+        return $qb->getQuery()->getResult();
+    }
+
     public function getFilteredQueryBuilder(array $filters = array())
     {
         $qb = $this->getEntityManager()->createQueryBuilder();

+ 7 - 1
src/Packagist/WebBundle/Model/FavoriteManager.php

@@ -69,12 +69,13 @@ class FavoriteManager
 
     public function getFaverCount(Package $package)
     {
-        return $this->redis->zcard('pkg:'.$package->getId().':fav');
+        return $this->redis->zcard('pkg:'.$package->getId().':fav') + $package->getGitHubStars();
     }
 
     public function getFaverCounts(array $packageIds)
     {
         $res = array();
+
         // TODO should be done with scripting when available
         foreach ($packageIds as $id) {
             if (ctype_digit((string) $id)) {
@@ -82,6 +83,11 @@ class FavoriteManager
             }
         }
 
+        $rows = $this->packageRepo->getGitHubStars($packageIds);
+        foreach ($rows as $row) {
+            $res[$row['id']] += $row['gitHubStars'];
+        }
+
         return $res;
     }
 

+ 75 - 1
src/Packagist/WebBundle/Package/Updater.php

@@ -17,6 +17,10 @@ use Composer\Package\PackageInterface;
 use Composer\Repository\RepositoryInterface;
 use Composer\Repository\InvalidRepositoryException;
 use Composer\Util\ErrorHandler;
+use Composer\Util\RemoteFilesystem;
+use Composer\Json\JsonFile;
+use Composer\Config;
+use Composer\IO\IOInterface;
 use Packagist\WebBundle\Entity\Author;
 use Packagist\WebBundle\Entity\Package;
 use Packagist\WebBundle\Entity\Tag;
@@ -85,8 +89,9 @@ class Updater
      * @param int $flags a few of the constants of this class
      * @param \DateTime $start
      */
-    public function update(Package $package, RepositoryInterface $repository, $flags = 0, \DateTime $start = null)
+    public function update(IOInterface $io, Config $config, Package $package, RepositoryInterface $repository, $flags = 0, \DateTime $start = null)
     {
+        $rfs = new RemoteFilesystem($io, $config);
         $blacklist = '{^symfony/symfony (2.0.[456]|dev-charset|dev-console)}i';
 
         if (null === $start) {
@@ -162,6 +167,10 @@ class Updater
             }
         }
 
+        if (preg_match('{^(?:git@|https?://)github.com[:/]([^/]+)/(.+?)(?:\.git|/)?$}i', $package->getRepository(), $match)) {
+            $this->updateGitHubInfo($rfs, $package, $match[1], $match[2]);
+        }
+
         $package->setUpdatedAt(new \DateTime);
         $package->setCrawledAt(new \DateTime);
         $em->flush();
@@ -380,4 +389,69 @@ class Updater
 
         return true;
     }
+
+    private function updateGitHubInfo(RemoteFilesystem $rfs, Package $package, $owner, $repo)
+    {
+        $baseApiUrl = 'https://api.github.com/repos/'.$owner.'/'.$repo;
+
+        try {
+            $repoData = JsonFile::parseJson($rfs->getContents('github.com', $baseApiUrl, false), $baseApiUrl);
+            $opts = ['http' => ['header' => ['Accept: application/vnd.github.v3.html']]];
+            $readme = $rfs->getContents('github.com', $baseApiUrl.'/readme', false, $opts);
+        } catch (\Exception $e) {
+            return;
+        }
+
+        if (!empty($readme)) {
+            $config = \HTMLPurifier_Config::createDefault();
+            $config->set('HTML.Allowed', 'a[href|target|rel|id],strong,b,em,i,strike,pre,code,p,ol,ul,li,br,h1,h2,h3,img[src|title|alt|width|height|style]');
+            $config->set('Attr.EnableID', true);
+            $config->set('Attr.AllowedFrameTargets', ['_blank']);
+            $purifier = new \HTMLPurifier($config);
+            $readme = $purifier->purify($readme);
+
+            $dom = new \DOMDocument();
+            $dom->loadHTML('<?xml encoding="UTF-8">' . $readme);
+
+            // Links can not be trusted
+            $links = $dom->getElementsByTagName('a');
+            foreach ($links as $link) {
+                $link->setAttribute('rel', 'nofollow');
+                if ('#' === substr($link->getAttribute('href'), 0, 1)) {
+                    $link->setAttribute('href', '#user-content-'.substr($link->getAttribute('href'), 1));
+                }
+            }
+
+            // remove first title as it's usually the project name which we don't need
+            if ($dom->getElementsByTagName('h1')->length) {
+                $first = $dom->getElementsByTagName('h1')->item(0);
+                $first->parentNode->removeChild($first);
+            } elseif ($dom->getElementsByTagName('h2')->length) {
+                $first = $dom->getElementsByTagName('h2')->item(0);
+                $first->parentNode->removeChild($first);
+            }
+
+            $readme = $dom->saveHTML();
+            $readme = substr($readme, strpos($readme, '<body>')+6);
+            $readme = substr($readme, 0, strrpos($readme, '</body>'));
+
+            $package->setReadme($readme);
+        }
+
+        if (!empty($repoData['language'])) {
+            $package->setLanguage($repoData['language']);
+        }
+        if (!empty($repoData['stargazers_count'])) {
+            $package->setGitHubStars($repoData['stargazers_count']);
+        }
+        if (!empty($repoData['subscribers_count'])) {
+            $package->setGitHubWatches($repoData['subscribers_count']);
+        }
+        if (!empty($repoData['network_count'])) {
+            $package->setGitHubForks($repoData['network_count']);
+        }
+        if (!empty($repoData['open_issues_count'])) {
+            $package->setGitHubOpenIssues($repoData['open_issues_count']);
+        }
+    }
 }

+ 54 - 10
src/Packagist/WebBundle/Resources/public/css/main.css

@@ -811,6 +811,7 @@ ul.packages .metadata-block:last-child {
   padding-right: 0;
   white-space: nowrap;
   overflow: hidden;
+  height: 30px;
 }
 
 .sortable .order-by-group .icon:first-child {
@@ -886,7 +887,7 @@ ul.packages .metadata-block:last-child {
 .package .details p > i:first-child {
     margin-right: 5px;
 }
-.package .details p {
+.package .details p, .package .facts p {
     margin: 0;
 }
 .package .details .maintainers {
@@ -915,14 +916,17 @@ ul.packages .metadata-block:last-child {
   white-space: nowrap;
 }
 
+.package .facts {
+    margin-top: 10px;
+}
 @media (max-width: 991px) and (min-width: 768px) {
-    .package .downloads {
+    .package .facts {
         margin-top: 0;
     }
 }
-.package .downloads span {
+.package .facts span {
     display: inline-block;
-    width: 60px;
+    width: 100px;
 }
 
 .package .tags a:before {
@@ -940,6 +944,41 @@ ul.packages .metadata-block:last-child {
 
 
 
+.package .readme {
+  padding-bottom: 20px;
+  max-height: 300px;
+  overflow-y: hidden;
+}
+.wrapper .content .package .readme h1:first-child,
+.wrapper .content .package .readme p:first-child,
+.wrapper .content .package .readme h2:first-child,
+.wrapper .content .package .readme h3:first-child {
+  margin-top: 0;
+}
+.wrapper .content .package .readme h1 {
+  font-size: 20px;
+  font-weight: 300;
+  padding: 0;
+  margin: 25px 0 10px;
+  line-height: 100%;
+}
+.wrapper .content .package .readme h2 {
+  font-size: 16px;
+  font-weight: 600;
+  padding: 0;
+  margin: 25px 0 10px;
+  line-height: 100%;
+}
+.wrapper .content .package .readme h3,
+.wrapper .content .package .readme h4,
+.wrapper .content .package .readme h5,
+.wrapper .content .package .readme h6 {
+  font-size: 14px;
+  font-weight: 600;
+  padding: 0;
+  margin: 15px 0 5px;
+  line-height: 100%;
+}
 
 .package .package-aside {
   border-top: 1px solid #f28d1a;
@@ -955,10 +994,10 @@ ul.packages .metadata-block:last-child {
 .package-aside .details {
   padding-left: 0;
 }
-.package .package-aside i {
+.package .package-aside i, .package .readme-expander i {
   color: #bbbfc4;
 }
-.package .package-aside i:hover {
+.package .package-aside i:hover, .package .readme-expander i:hover {
   color: #a5aab0;
 }
 
@@ -982,10 +1021,15 @@ ul.packages .metadata-block:last-child {
   padding: 0;
   margin: 0;
 }
-.package .versions-expander {
+.package .versions-expander, .package .readme-expander {
   text-align: center;
   cursor: pointer;
-  padding: 10px 0 0;
+  margin: 10px 0 0;
+  line-height: 25px;
+}
+.package .readme-expander {
+  margin-bottom: 20px;
+  background: #fff;
 }
 .package .versions .version {
   padding: 2px 0 2px 5px;
@@ -1027,7 +1071,7 @@ ul.packages .metadata-block:last-child {
       background: #fff;
       border-radius: 2px;
       border-top: 0;
-      padding: 10px 0 0 0;
+      padding: 10px 0;
       margin-left: -15px;
       margin-right: -15px;
     }
@@ -1047,7 +1091,7 @@ ul.packages .metadata-block:last-child {
     }
 }
 @media (max-width: 767px) {
-  .package-aside .downloads {
+  .package-aside .facts {
     padding-left: 0;
   }
   .wrapper-footer {

+ 30 - 3
src/Packagist/WebBundle/Resources/public/js/view.js

@@ -72,7 +72,7 @@
             data: data,
             type: 'PUT',
             success: function () {
-                window.location.href = window.location.href;
+                document.location.reload(true);
             },
             context: $('.package .force-update')[0]
         }).complete(function () { submit.removeClass('loading'); });
@@ -128,12 +128,39 @@
         forceUpdatePackage(null, true);
     }
 
+    $('.readme a').on('click', function (e) {
+        var targetEl,
+            href = e.target.getAttribute('href');
+
+        if (href.substr(0, 1) === '#') {
+            targetEl = $(href);
+            if (targetEl.length) {
+                window.scrollTo(0, targetEl.offset().top - 70);
+                e.preventDefault();
+                e.stopImmediatePropagation();
+            }
+        }
+    });
+
     var versionsList = $('.package .versions')[0];
     if (versionsList.offsetHeight < versionsList.scrollHeight) {
         $('.package .versions-expander').removeClass('hidden').on('click', function () {
-            $(this).addClass('hidden')
+            $(this).addClass('hidden');
             $(versionsList).css('overflow-y', 'visible')
-                .css('max-height', 'auto');
+                .css('max-height', 'inherit');
         });
     }
+
+    var readme = $('.package .readme')[0];
+    if (readme && readme.offsetHeight < readme.scrollHeight) {
+        $('.package .readme-expander').removeClass('hidden').on('click', function () {
+            $(this).addClass('hidden');
+            $(readme).css('overflow-y', 'visible')
+                .css('max-height', 'inherit');
+        });
+        // auto-expand when contracting doesn't hide enough to make it worth it
+        if (readme.offsetHeight > readme.scrollHeight - 200) {
+            $('.package .readme-expander').click();
+        }
+    }
 }(jQuery, humane));

+ 23 - 5
src/Packagist/WebBundle/Resources/views/Web/viewPackage.html.twig

@@ -146,11 +146,18 @@
                             {% endif %}
                         </div>
 
-                        <p class="downloads col-xs-12 col-sm-6 col-md-12">
-                            <span>Overall:</span> {% if downloads.total is defined %}{{ downloads.total|number_format(0, '.', '&#8201;')|raw }} install{{ downloads.total == 1 ? '' : 's' }}{% else %}N/A{% endif %}<br />
-                            <span>30 days:</span> {% if downloads.monthly is defined %}{{ downloads.monthly|number_format(0, '.', '&#8201;')|raw }} install{{ downloads.monthly == 1 ? '' : 's' }}{% else %}N/A{% endif %}<br />
-                            <span>Today:</span> {% if downloads.daily is defined %}{{ downloads.daily|number_format(0, '.', '&#8201;')|raw }} install{{ downloads.daily == 1 ? '' : 's' }}{% else %}N/A{% endif %}<br />
-                        </p>
+                        <div class="facts col-xs-12 col-sm-6 col-md-12">
+                            <p><strong>Installs</strong></p>
+                            <p><span>Overall:</span> {% if downloads.total is defined %}{{ downloads.total|number_format(0, '.', '&#8201;')|raw }}{% else %}N/A{% endif %}</p>
+                            <p><span>30 days:</span> {% if downloads.monthly is defined %}{{ downloads.monthly|number_format(0, '.', '&#8201;')|raw }}{% else %}N/A{% endif %}</p>
+                            <p><span>Today:</span> {% if downloads.daily is defined %}{{ downloads.daily|number_format(0, '.', '&#8201;')|raw }}{% else %}N/A{% endif %}</p>
+                            {% if package.gitHubWatches != null %}<br>{% endif %}
+                            {% if package.language != null %}<p><span>Language:</span> {{ package.language }}</p>{% endif %}
+                            {% if package.gitHubStars != null %}<p><span>Stars:</span> {{ package.gitHubStars|number_format(0, '.', '&#8201;')|raw }}</p>{% endif %}
+                            {% if package.gitHubWatches != null %}<p><span>Watches:</span> {{ package.gitHubWatches|number_format(0, '.', '&#8201;')|raw }}</p>{% endif %}
+                            {% if package.gitHubForks != null %}<p><span>Forks:</span> {{ package.gitHubForks|number_format(0, '.', '&#8201;')|raw }}</p>{% endif %}
+                            {% if package.gitHubOpenIssues != null %}<p><span>Open Issues:</span> {{ package.gitHubOpenIssues|number_format(0, '.', '&#8201;')|raw }}</p>{% endif %}
+                        </div>
                     </div>
                 </div>
             </div>
@@ -228,5 +235,16 @@
                 <p class="col-xs-12">This package has no released version yet, and little information is available.</p>
             {% endif %}
         </div>
+
+        {% if package.readme != null %}
+            <hr class="clearfix">
+            <div class="readme">
+                <h1>README</h1>
+                {{ package.readme|raw }}
+            </div>
+            <div class="hidden readme-expander">
+                <i class="glyphicon glyphicon-chevron-down"></i>
+            </div>
+        {% endif %}
     </div>
 {% endblock %}

+ 1 - 0
src/Packagist/WebBundle/Resources/views/macros.html.twig

@@ -10,6 +10,7 @@
                 <div class="col-xs-12 package-item">
                     <div class="row">
                         <div class="col-sm-9 col-lg-10">
+                            {% if package.language is defined and package.language != null %}<p class="pull-right language">{{ package.language }}</p>{% endif %}
                             <h4 class="font-bold">
                                 <a href="{{ packageUrl }}">{{ package.name }}</a>
                                 {% if package.id is not numeric or package.name == 'nelmio/alice' %}