diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..c95d9e06d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# From: +# - https://github.com/rhysd/actionlint/issues/228#issuecomment-1272493095 +# - https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot + +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/cabal-mtl-2.3.yml b/.github/workflows/cabal-mtl-2.3.yml new file mode 100644 index 000000000..423f1dd50 --- /dev/null +++ b/.github/workflows/cabal-mtl-2.3.yml @@ -0,0 +1,62 @@ +name: Cabal build with mtl-2.3 +on: + push: + branches: + - master + - ci* + pull_request: + branches: + - master + - ci* + +defaults: + run: + shell: bash + +jobs: + build: + name: Build with mtl-2.3 + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + + - name: Environment settings based on the Haskell setup + run: | + GHC_VER=$(ghc --numeric-version) + CABAL_VER=$(cabal --numeric-version) + echo "GHC_VER = ${GHC_VER}" + echo "CABAL_VER = ${CABAL_VER}" + echo "GHC_VER=${GHC_VER}" >> "${GITHUB_ENV}" + echo "CABAL_VER=${CABAL_VER}" >> "${GITHUB_ENV}" + + - name: Install necessary deps + run: | + sudo apt-get update + sudo apt-get install -y libgd-dev libpng-dev libjpeg-dev libfontconfig-dev libfreetype-dev libexpat1-dev + + - uses: actions/checkout@v3 + + - name: Cache build + uses: actions/cache@v3 + with: + path: | + ~/.cabal + dist-newstyle + key: cabal-${{ env.CABAL_VER }}-ghc-${{ env.GHC_VER }}-commit-${{ github.sha }} + restore-keys: | + cabal-${{ env.CABAL_VER }}-ghc-${{ env.GHC_VER }}-commit- + + - name: Prepare cabal + run: | + cabal update + + - name: Build dependencies w/o tests with mtl-2.3 + # 2022-12-30: 'transformers >= 0.6' is needed because of happstack-server + run: | + cabal build --dependencies-only -O0 --disable-tests --constraint 'mtl >= 2.3.1' --constraint 'transformers >= 0.6' --allow-newer='Cabal:mtl' --allow-newer='Cabal:transformers' + + - name: Build w/o tests with mtl-2.3 + # 2022-12-30: 'transformers >= 0.6' is needed because of happstack-server + run: | + cabal build -O0 --disable-tests --constraint 'mtl >= 2.3.1' --constraint 'transformers >= 0.6' --allow-newer='Cabal:mtl' --allow-newer='Cabal:transformers' diff --git a/.github/workflows/ci.yml.deactivated b/.github/workflows/ci.yml.deactivated deleted file mode 100644 index a6a0d2285..000000000 --- a/.github/workflows/ci.yml.deactivated +++ /dev/null @@ -1,74 +0,0 @@ -# modified from https://github.com/jgm/pandoc/blob/master/.github/workflows/ci.yml -name: CI - -on: - push: - branches: - - '**' - paths-ignore: [] - pull_request: - paths-ignore: [] - -jobs: - linux: - - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - versions: - - ghc: '9.2.2' - cabal: '3.6' - - ghc: '9.0.2' - cabal: '3.6' - - ghc: '8.10.7' - cabal: '3.6' - - ghc: '8.8.4' - cabal: '3.6' - steps: - - uses: actions/checkout@v2 - - - name: Install recent cabal/ghc - uses: haskell/actions/setup@v1 - with: - ghc-version: ${{ matrix.versions.ghc }} - cabal-version: ${{ matrix.versions.cabal }} - - # declare/restore cached things - # caching doesn't work for scheduled runs yet - # https://github.com/actions/cache/issues/63 - - - name: Cache cabal global package db - id: cabal-global - uses: actions/cache@v2 - with: - path: | - ~/.cabal - key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-global-${{ hashFiles('cabal.project') }} - - - name: Cache cabal work - id: cabal-local - uses: actions/cache@v2 - with: - path: | - dist-newstyle - key: ${{ runner.os }}-${{ matrix.versions.ghc }}-${{ matrix.versions.cabal }}-cabal-local - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install libbrotli-dev - cabal update - cabal build --dependencies-only --enable-tests --disable-optimization - - - name: Build - run: | - cabal build --enable-tests --disable-optimization 2>&1 | tee build.log - - - name: Test - run: | - cabal test --enable-tests --disable-optimization - - - name: Haddock - run: | - cabal haddock --enable-tests --disable-optimization diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 0aa3a3c9e..bd5f639b1 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -8,29 +8,40 @@ # # For more information, see https://github.com/haskell-CI/haskell-ci # -# version: 0.14.3.20220416 +# version: 0.15.20221225 # -# REGENDATA ("0.14.3.20220416",["github","hackage-server.cabal"]) +# REGENDATA ("0.15.20221225",["github","hackage-server.cabal"]) # name: Haskell-CI on: - - push - - pull_request + push: + branches: + - master + - ci* + pull_request: + branches: + - master + - ci* jobs: linux: name: Haskell-CI - Linux - ${{ matrix.compiler }} - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 timeout-minutes: 60 container: - image: buildpack-deps:focal + image: buildpack-deps:jammy continue-on-error: ${{ matrix.allow-failure }} strategy: matrix: include: - - compiler: ghc-9.2.2 + - compiler: ghc-9.4.4 compilerKind: ghc - compilerVersion: 9.2.2 + compilerVersion: 9.4.4 + setup-method: ghcup + allow-failure: false + - compiler: ghc-9.2.5 + compilerKind: ghc + compilerVersion: 9.2.5 setup-method: ghcup allow-failure: false - compiler: ghc-9.0.2 @@ -46,7 +57,7 @@ jobs: - compiler: ghc-8.8.4 compilerKind: ghc compilerVersion: 8.8.4 - setup-method: hvr-ppa + setup-method: ghcup allow-failure: false fail-fast: false steps: @@ -54,23 +65,13 @@ jobs: run: | apt-get update apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 - if [ "${{ matrix.setup-method }}" = ghcup ]; then - mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.17.5/x86_64-linux-ghcup-0.1.17.5 > "$HOME/.ghcup/bin/ghcup" - chmod a+x "$HOME/.ghcup/bin/ghcup" - "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" - "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 - apt-get update - apt-get install -y libbrotli-dev - else - apt-add-repository -y 'ppa:hvr/ghc' - apt-get update - apt-get install -y "$HCNAME" libbrotli-dev - mkdir -p "$HOME/.ghcup/bin" - curl -sL https://downloads.haskell.org/ghcup/0.1.17.5/x86_64-linux-ghcup-0.1.17.5 > "$HOME/.ghcup/bin/ghcup" - chmod a+x "$HOME/.ghcup/bin/ghcup" - "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 - fi + mkdir -p "$HOME/.ghcup/bin" + curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup" + chmod a+x "$HOME/.ghcup/bin/ghcup" + "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) + "$HOME/.ghcup/bin/ghcup" install cabal 3.6.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false) + apt-get update + apt-get install -y libbrotli-dev libgd-dev libpng-dev libjpeg-dev libfontconfig-dev libfreetype-dev libexpat1-dev env: HCKIND: ${{ matrix.compilerKind }} HCNAME: ${{ matrix.compiler }} @@ -82,23 +83,14 @@ jobs: echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV" echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV" HCDIR=/opt/$HCKIND/$HCVER - if [ "${{ matrix.setup-method }}" = ghcup ]; then - HC=$HOME/.ghcup/bin/$HCKIND-$HCVER - echo "HC=$HC" >> "$GITHUB_ENV" - echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV" - echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV" - echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV" - else - HC=$HCDIR/bin/$HCKIND - echo "HC=$HC" >> "$GITHUB_ENV" - echo "HCPKG=$HCDIR/bin/$HCKIND-pkg" >> "$GITHUB_ENV" - echo "HADDOCK=$HCDIR/bin/haddock" >> "$GITHUB_ENV" - echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV" - fi - + HC=$HOME/.ghcup/bin/$HCKIND-$HCVER + echo "HC=$HC" >> "$GITHUB_ENV" + echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV" + echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV" + echo "CABAL=$HOME/.ghcup/bin/cabal-3.6.2.0 -vnormal+nowrap" >> "$GITHUB_ENV" HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" - echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" + if [ $((HCNUMVER >= 90400)) -ne 0 ] ; then echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" ; else echo "ARG_TESTS=--disable-tests" >> "$GITHUB_ENV" ; fi echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV" echo "HEADHACKAGE=false" >> "$GITHUB_ENV" echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV" @@ -152,7 +144,7 @@ jobs: chmod a+x $HOME/.cabal/bin/cabal-plan cabal-plan --version - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: source - name: initial cabal.project for sdist @@ -180,7 +172,7 @@ jobs: echo " ghc-options: -Werror=missing-methods" >> cabal.project cat >> cabal.project <> cabal.project.local + $HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(Cabal|hackage-server|parsec|process|text)$/; }' >> cabal.project.local cat cabal.project cat cabal.project.local - name: dump install plan @@ -188,7 +180,7 @@ jobs: $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all cabal-plan - name: cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} path: ~/.cabal/store @@ -205,14 +197,14 @@ jobs: $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always - name: tests run: | - $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct + if [ $((HCNUMVER >= 90400)) -ne 0 ] ; then $CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct ; fi - name: cabal check run: | cd ${PKGDIR_hackage_server} || false ${CABAL} -vnormal check - name: haddock run: | - $CABAL v2-haddock $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + $CABAL v2-haddock --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all - name: unconstrained build run: | rm -f cabal.project.local diff --git a/.github/workflows/nix-shell.yml b/.github/workflows/nix-shell.yml index 746f43f1d..f6c20790c 100644 --- a/.github/workflows/nix-shell.yml +++ b/.github/workflows/nix-shell.yml @@ -1,19 +1,27 @@ -# https://nix.dev/tutorials/continuous-integration-github-actions name: "Test nix-shell" on: push: branches: - - '**' - paths-ignore: [] + - master + - ci* pull_request: - paths-ignore: [] - + branches: + - master + - ci* jobs: nix-shell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 - - uses: cachix/install-nix-action@v16 + - uses: actions/checkout@v3.1.0 + - uses: cachix/install-nix-action@v18 with: nix_path: nixpkgs=channel:nixos-21.11 - - run: nix-shell --pure --run "cabal update && cabal build all --enable-tests" + extra_nix_config: | + trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hackage-server.cachix.org-1:iw0iRh6+gsFIrxROFaAt5gKNgIHejKjIfyRdbpPYevY= + substituters = https://hydra.iohk.io https://cache.nixos.org/ https://hackage-server.cachix.org/ + - uses: cachix/cachix-action@v12 + with: + # https://nix.dev/tutorials/continuous-integration-github-actions#setting-up-github-actions + name: hackage-server + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - run: nix-shell --pure --run ./.github/workflows/test-nix-shell.sh \ No newline at end of file diff --git a/.github/workflows/test-nix-shell.sh b/.github/workflows/test-nix-shell.sh new file mode 100755 index 000000000..b48799885 --- /dev/null +++ b/.github/workflows/test-nix-shell.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cabal update hackage.haskell.org,2022-08-27T00:00:00Z +cabal build all --enable-tests diff --git a/README.md b/README.md index 9c0bf0d48..057aa536a 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,5 @@ This is the branch for the "official" Hackage server at hackage.haskell.org. Mos # General Documentation ======= [![Build Status](https://travis-ci.org/haskell/hackage-server.png?branch=master)](https://travis-ci.org/haskell/hackage-server) -[![Build status](https://github.com/haskell/hackage-server/actions/workflows/ci.yml/badge.svg)](https://github.com/haskell/hackage-server/actions/workflows/ci.yml) +[![Build status](https://github.com/haskell/hackage-server/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/haskell/hackage-server/actions/workflows/haskell-ci.yml) [![Build status](https://github.com/haskell/hackage-server/actions/workflows/nix-shell.yml/badge.svg)](https://github.com/haskell/hackage-server/actions/workflows/nix-shell.yml) diff --git a/benchmarks/RevDeps.hs b/benchmarks/RevDeps.hs new file mode 100644 index 000000000..4585306ab --- /dev/null +++ b/benchmarks/RevDeps.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE ScopedTypeVariables , TypeApplications #-} +module Main where + +import Control.Monad (replicateM) +import Data.Containers.ListUtils (nubOrd) +import qualified Data.Vector as Vector +import Distribution.Package (packageName) +import Distribution.Server.Features.ReverseDependencies.State (constructReverseIndex, getDependenciesFlat) +import Distribution.Server.Packages.PackageIndex as PackageIndex + +import Gauge.Benchmark (nfAppIO, bench) +import Gauge.Main (defaultMain) +import System.Random.Stateful + +import RevDepCommon (Package(..), packToPkgInfo, TestPackage(..)) + +randomPacks + :: forall m g. StatefulGen g m + => g + -> Int + -> Vector.Vector (Package TestPackage) + -> m (Vector.Vector (Package TestPackage)) +randomPacks gen limit generated | length generated < limit = do + makeNewPack <- uniformM gen -- if not new pack, just make a new version of an existing + toInsert <- + if makeNewPack || generated == mempty + then + Package + <$> pure (TestPackage (fromIntegral @Int @Word $ Vector.length generated)) + <*> uniformRM (0, 10) gen + <*> pure mempty + else do + prevIdx <- uniformRM (0, length generated - 1) gen + let Package { pName = prevName } = generated Vector.! prevIdx + (prevNamePacks, nonPrevName) = Vector.partition ((== prevName) . pName) generated + depPacks <- + if mempty /= nonPrevName + then do + -- TODO this should have an expected amount of deps equal to what is actually on hackage. what is it? + numOfDeps <- uniformRM (1, min (length nonPrevName - 1) 7) gen + indicesMayDuplicate <- replicateM numOfDeps (uniformRM (0, length nonPrevName - 1) gen) + let indices = nubOrd indicesMayDuplicate + pure $ map (nonPrevName Vector.!) indices + else + pure [] + let + newVersion = + if mempty /= prevNamePacks + then 1 + maximum (fmap pVersion prevNamePacks) + else 0 + pure $ + Package + { pName = prevName + , pVersion = newVersion + , pDeps = map pName depPacks + } + let added = generated <> pure toInsert + randomPacks gen limit added +randomPacks _ _ generated = pure generated + +main :: IO () +main = do + packs :: Vector.Vector (Package TestPackage) <- randomPacks globalStdGen 20000 mempty + let idx = PackageIndex.fromList $ map packToPkgInfo (Vector.toList packs) + let revs = constructReverseIndex idx + let numPacks = length packs + defaultMain $ + (:[]) $ + bench "get transitive dependencies for one randomly selected package" $ + flip nfAppIO revs $ \revs' -> do + select <- uniformRM (0, numPacks - 1) globalStdGen + -- TODO why are there so many transitive deps? + pure $ length $ + getDependenciesFlat + (packageName $ packToPkgInfo (packs Vector.! select)) + revs' diff --git a/cabal.haskell-ci b/cabal.haskell-ci index bc8774bef..5e5272c5d 100644 --- a/cabal.haskell-ci +++ b/cabal.haskell-ci @@ -1,14 +1,26 @@ -installed: +all -Cabal -text -parsec +branches: master ci* --- -- irc-channels works with GHA, but why send to a channel --- -- when one can subscribe to github notifications? --- irc-channels: irc.libera.chat#hackage +installed: +all -Cabal -text -parsec -process + -- Cabal-3.8.1.0 wants process-1.6.14 or newer + +-- Did not help to salvage ghc-9.2 and below: +-- installed: -all +ghc -- Does not work with GHA: -- -- allow failures with ghc-7.6 and ghc-7.8 -- allow-failures: <7.9 --- Use Ubuntu 20.04 -distribution: focal +-- Use Ubuntu 22.04 +distribution: jammy + +apt: libbrotli-dev + +-- Make sure the haddock step is included, +-- even though we don't define any library. +haddock-components: all + -- since haskell-ci 0.15.20220822 -apt: libbrotli-dev \ No newline at end of file +tests: >= 9.4 + -- parallel-doctest uses the ghc package + -- and thus does not build with Cabal-3.8.1.0 below GHC 9.4 + -- See: https://github.com/haskell/cabal/issues/8554 diff --git a/cabal.project b/cabal.project index 9c11e5678..8ba4a61b7 100644 --- a/cabal.project +++ b/cabal.project @@ -11,6 +11,17 @@ packages: . allow-newer: rss:time, rss:base +-- Andreas, 2022-10-28: `Cabal-3.8.1.0` wants `process >= 1.6.14` +-- which is too new for the `ghc < 9.4` package that is pulled in +-- by `doctest-parallel`. +-- Since, Cabal-3.8.1.0 has no reason to want such a new version +-- of process, we can solve the conflict here by allowing +-- `Cabal` to use the shipped version of `process`. +-- This workaround can be removed once `Cabal-3.8` drops +-- its (unreasonable) constraint on `process`. +-- See: https://github.com/haskell/cabal/issues/8554 +allow-older: Cabal:process + ----------------------------------------------------------------------------- -- Anti-constraints diff --git a/datafiles/static/browse.js b/datafiles/static/browse.js index 905239afc..868768a44 100644 --- a/datafiles/static/browse.js +++ b/datafiles/static/browse.js @@ -133,6 +133,7 @@ const replaceRows = (response) => { tr.appendChild(createSimpleText(row.description)); tr.appendChild(createTags(row.tags)); tr.appendChild(createLastUpload(row.lastUpload)); + tr.appendChild(createSimpleText(row.lastVersion)); tr.appendChild(createMaintainers(row.maintainers)); l.appendChild(tr); } @@ -252,14 +253,17 @@ const createPageLink = (num) => { return a; }; -const createPrevNext = (prevNextNum, cond, txt) => { - const el = d.createElement(cond ? "span" : "a"); - el.setAttribute("href", "#"); - el.addEventListener('click', (evt) => { - evt.preventDefault(); - changePage(prevNextNum); - }); - if (cond) el.classList.add("disabled"); +const createPrevNext = (prevNextNum, hasLink, txt) => { + const el = d.createElement("a"); + + if(hasLink) { + el.setAttribute("href", "#"); + el.addEventListener('click', (evt) => { + evt.preventDefault(); + changePage(prevNextNum); + }); + } + el.appendChild(d.createTextNode(txt)); return el; }; @@ -275,7 +279,7 @@ const createPaginator = () => { const pag = d.createElement("div"); pag.classList.add("paginator"); - pag.appendChild(createPrevNext(state.page - 1, state.page === 0, "Previous")); + pag.appendChild(createPrevNext(state.page - 1, state.page !== 0, "Previous")); // note that page is zero-indexed if (maxPage <= 4) { // No ellipsis @@ -307,7 +311,8 @@ const createPaginator = () => { pag.appendChild(createPageLink(maxPage)); } const isNowOnLastPage = state.page === maxPage; - pag.appendChild(createPrevNext(state.page + 1, isNowOnLastPage, "Next")); + + pag.appendChild(createPrevNext(state.page + 1, !isNowOnLastPage, "Next")); return pag; }; diff --git a/datafiles/static/graph/graph.css b/datafiles/static/graph/graph.css new file mode 100644 index 000000000..4ad5cafc2 --- /dev/null +++ b/datafiles/static/graph/graph.css @@ -0,0 +1 @@ + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.label,sub,sup{vertical-align:baseline}.search ul,hr{box-sizing:content-box}hr,img{border:0}body,figure{margin:0}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu{float:left}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}.form-control-feedback,.navigation-help,.node-hover-list,.node-hover-tooltip,.steering,a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.glyphicon,address{font-style:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.navbar-toggle,.open>.dropdown-toggle.btn-danger,.open>.dropdown-toggle.btn-default,.open>.dropdown-toggle.btn-info,.open>.dropdown-toggle.btn-primary,.open>.dropdown-toggle.btn-warning{background-image:none}@font-face{font-family:'Glyphicons Halflings';src:url(glyphicons-halflings-regular.eot);src:url(glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(glyphicons-halflings-regular.woff2) format('woff2'),url(glyphicons-halflings-regular.woff) format('woff'),url(glyphicons-halflings-regular.ttf) format('truetype'),url(glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before,.glyphicon-btc:before,.glyphicon-xbt:before{content:"\e227"}.glyphicon-jpy:before,.glyphicon-yen:before{content:"\00a5"}.glyphicon-rub:before,.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-size:14px;line-height:1.42857143;color:#333}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}pre code,table{background-color:transparent}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}dl,ol,ul{margin-top:0}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}address,dl{margin-bottom:20px}ol,ul{margin-bottom:10px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#777}legend,pre{display:block;color:#333}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{padding:9.5px;margin:0 0 10px;font-size:13px;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}.container,.container-fluid{margin-right:auto;margin-left:auto}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-left:15px;padding-right:15px}.pre-scrollable{overflow-y:scroll}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-left:-15px;margin-right:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}caption{padding-top:8px;padding-bottom:8px;color:#777}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{margin:0;min-width:0}legend{width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:14px;line-height:1.42857143;color:#555;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:7px}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.form-group-sm .form-control,.input-sm{padding:5px 10px;border-radius:3px;font-size:12px}.input-sm{height:30px;line-height:1.5}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:10px 16px;font-size:18px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;line-height:1.3333333}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;line-height:1.3333333}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:400;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu-right,.dropdown-menu.pull-right{left:auto;right:0}.dropdown-header,.dropdown-menu>li>a{display:block;padding:3px 20px;line-height:1.42857143;white-space:nowrap}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{clear:both;font-weight:400;color:#333}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#337ab7}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{font-size:12px;color:#777}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.nav>li,.nav>li>a{display:block;position:relative}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}}.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:768px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:8px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.breadcrumb>li,.pagination{display:inline-block}.btn .badge,.btn .label{top:-1px;position:relative}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{padding-left:0;margin:20px 0;border-radius:4px}.pager li,.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.close,.list-group-item>.badge,.pager .next>a,.pager .next>span{float:right}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.badge,.label{font-weight:700;line-height:1;white-space:nowrap;text-align:center}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}a.badge:focus,a.badge:hover,a.label:focus,a.label:hover{color:#fff;cursor:pointer;text-decoration:none}.label{display:inline;padding:.2em .6em .3em;font-size:75%;border-radius:.25em}.label:empty{display:none}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.media-object,.thumbnail{display:block}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#eee}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.alert,.thumbnail{margin-bottom:20px}.alert .alert-link,.close{font-weight:700}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-left:auto;margin-right:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.modal,.modal-backdrop{top:0;right:0;bottom:0;left:0}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.carousel-indicators li,.in-degree,.node-focus,.out-degree,button.close{cursor:pointer}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-title{margin-top:0;font-size:16px}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{font-size:21px;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.popover,.tooltip{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;text-decoration:none}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;background:0 0;border:0;-webkit-appearance:none}.modal-content,.popover{background-clip:padding-box}.modal{display:none;position:fixed;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;text-align:left;text-align:start;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px}.tooltip.top-right .tooltip-arrow{left:5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{border-width:0 5px 5px;border-bottom-color:#000;top:0}.tooltip.bottom .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px}.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;text-align:start;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.carousel-caption,.carousel-control{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6);text-align:center}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.left>.arrow:after,.popover.right>.arrow:after{content:" ";bottom:-10px}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{left:1px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;border-right-width:0;border-left-color:#fff}.carousel-inner{overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:focus,.carousel-control:hover{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;background-color:#000\9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{content:" ";display:table}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0}.affix{position:fixed}.steering,.steering .inner{position:absolute;top:50%;left:50%}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}.steering{width:200px;height:200px;z-index:1;margin-left:-100px;margin-top:-100px;border-radius:200px;border:2px solid rgba(255,255,255,.6)}.steering .inner{width:100px;height:100px;margin-left:-50px;margin-top:-50px;border-radius:100px;border:2px solid rgba(255,255,255,.2)}.steering .steering-help{position:fixed;color:rgba(255,255,255,.9);bottom:12px;font-family:'PT Sans',sans-serif;font-size:large;left:50%;margin-left:-187px}#app,.graph-full-size,.label,.navigation-help,.node-details,.node-hover-list,.search,.window-container{position:absolute}.node-details h2,.node-details h4{margin:0;font-family:'PT Sans',sans-serif}.node-details{width:395px;left:16px;bottom:12px;padding:12px;z-index:1;font-family:'PT Sans',sans-serif;background:rgba(0,0,0,.85);border:1px solid grey;color:#fff}.node-details h4{display:block;display:-webkit-box;max-width:400px;height:50.4px;font-size:18px;line-height:1.4;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.node-details h2{text-align:center}.node-details .small{text-align:center;color:#999}@media (max-width:768px){.node-details{right:12px;width:inherit}.node-details .info-block{padding-left:0}.node-details .info-block a{display:inline-block;margin:0 7px}}.node-details .info-block a{font-size:18px}.github-avatar-detail{width:50px;margin-left:-16px;margin-right:8px;float:left}.search{top:12px;z-index:1}.search .search-results{padding-top:12px;background:rgba(0,0,0,.85)}.search .search-results h4{margin:0 0 5px;padding:0 0 5px 20px}.search .scroll-wrapper{border-top:1px solid grey;height:250px;overflow:hidden}.search ul{overflow-y:scroll;overflow-x:hidden;width:100%;height:100%;color:#fff;list-style:none;margin:0;padding:0 20px 0 0}.search ul li{height:25px}#app,.graph-full-size,.search ul li a{width:100%;height:100%}.search ul li a{opacity:.5;color:#fff;text-decoration:none;display:block;margin:0;padding:0 0 0 20px;line-height:25px;white-space:nowrap}.search ul li a:focus,.search ul li a:hover{text-decoration:none;opacity:1;background:rgba(26,26,26,.95)}.search .search-form input{width:100%;background:rgba(26,26,26,.5);color:#fff;border-radius:0;border:0}.search .search-form .input-group{border:1px solid grey}.search .search-form .input-group.focused,.search .search-form .input-group:hover{border-color:#fff}.search .search-form .input-group.focused button.btn,.search .search-form .input-group:hover button.btn{background:#66afe9}.search .search-form button.btn{color:#fff;border-radius:0;background:grey}.search h4{color:#fff}.search h4 .small{opacity:.5}body{background-color:#000;font-family:Roboto,sans-serif}#app{overflow:auto}.graph-full-size{top:0;left:0;overflow:hidden}h1,h2{color:#efefef}h4{font-family:'PT Sans',sans-serif}.window-container{background-color:rgba(0,0,0,.8);color:#7F7F7F}.window-list-content{overflow-y:auto;overflow-x:hidden;max-height:200px}.search-results-window{left:16px;top:47px;padding:10px;width:395px}.degree-results-window{left:16px;bottom:87px;padding:10px;width:395px}.no-overflow{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.window-title strong{color:#fff;font-weight:400;font-size:1.2em}.window-title .node-name{color:#fff}.label{color:#fff;background:#000;z-index:1}.loading{padding:10px;left:10px;top:48px}.about{padding:10px;right:10px;top:0}@media (max-width:768px){.degree-results-window{display:none}.about{padding:10px;right:13px;bottom:22px;top:inherit}}.node-hover-tooltip{position:absolute;background:rgba(0,0,0,.8);color:#fff;padding:4px 10px;min-width:200px;border:1px solid grey}.in-degree,.window-indegree{color:#90EE90}.node-hover-tooltip span{margin:0 5px}.out-degree,.window-outdegree{color:#F08080}.node-hover-list{background:#000;right:-20px;bottom:10px;height:150px;color:#fff;padding:4px;width:220px;overflow-y:scroll}.node-hover-list ul{padding:0}.node-hover-list li{list-style-type:none}.vcenter{display:inline-block;vertical-align:middle;float:none}a.reset-color,a.reset-color:focus,a.reset-color:hover{text-decoration:none;color:inherit}a.media:focus,a.media:hover{text-decoration:none;color:#FF008C;border:none}a.media{color:#fff;background-color:#000;border:none}.error{color:pink}.error-details{color:wheat}.navigation-help{background-color:rgba(0,0,0,.55);padding:7px;right:20px;top:50%;transform:translateY(-50%);font-family:Roboto,sans-serif}.navigation-help h3{color:#fff;border-bottom:1px solid #fff;padding-bottom:6px;margin-top:2px}.navigation-help code{font-family:'PT Sans',sans-serif;font-size:100%;color:#fff;background-color:inherit}.navigation-help code.important-key{color:#90EE90}.navigation-help td{color:#999;text-align:right}.navigation-help td:nth-child(even){text-align:left}.navigation-help tr.spacer-row{border-bottom:10px solid transparent} diff --git a/datafiles/static/graph/tmpl.js b/datafiles/static/graph/tmpl.js new file mode 100644 index 000000000..88968fd95 --- /dev/null +++ b/datafiles/static/graph/tmpl.js @@ -0,0 +1,86 @@ +/* + * JavaScript Templates + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Inspired by John Resig's JavaScript Micro-Templating: + * http://ejohn.org/blog/javascript-micro-templating/ + */ + +/*global document, define, module */ + +;(function ($) { + 'use strict' + var tmpl = function (str, data) { + var f = !/[^\w\-\.:]/.test(str) + ? tmpl.cache[str] = tmpl.cache[str] || tmpl(tmpl.load(str)) + : new Function(// eslint-disable-line no-new-func + tmpl.arg + ',tmpl', + 'var _e=tmpl.encode' + tmpl.helper + ",_s='" + + str.replace(tmpl.regexp, tmpl.func) + "';return _s;" + ) + return data ? f(data, tmpl) : function (data) { + return f(data, tmpl) + } + } + tmpl.cache = {} + tmpl.load = function (id) { + return document.getElementById(id).innerHTML + } + tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g + tmpl.func = function (s, p1, p2, p3, p4, p5) { + if (p1) { // whitespace, quote and backspace in HTML context + return { + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + ' ': ' ' + }[p1] || '\\' + p1 + } + if (p2) { // interpolation: {%=prop%}, or unescaped: {%#prop%} + if (p2 === '=') { + return "'+_e(" + p3 + ")+'" + } + return "'+(" + p3 + "==null?'':" + p3 + ")+'" + } + if (p4) { // evaluation start tag: {% + return "';" + } + if (p5) { // evaluation end tag: %} + return "_s+='" + } + } + tmpl.encReg = /[<>&"'\x00]/g + tmpl.encMap = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' + } + tmpl.encode = function (s) { + return (s == null ? '' : '' + s).replace( + tmpl.encReg, + function (c) { + return tmpl.encMap[c] || '' + } + ) + } + tmpl.arg = 'o' + tmpl.helper = ",print=function(s,e){_s+=e?(s==null?'':s):_e(s);}" + + ',include=function(s,d){_s+=tmpl(s,d);}' + if (typeof define === 'function' && define.amd) { + define(function () { + return tmpl + }) + } else if (typeof module === 'object' && module.exports) { + module.exports = tmpl + } else { + $.tmpl = tmpl + } +}(this)) diff --git a/datafiles/static/graph/vivagraph.js b/datafiles/static/graph/vivagraph.js new file mode 100644 index 000000000..673125611 --- /dev/null +++ b/datafiles/static/graph/vivagraph.js @@ -0,0 +1,114 @@ +!function(S){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=S();else if("function"==typeof define&&define.amd)define([],S);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self);e.Viva=S()}}(function(){return function e(g,l,d){function m(a,c){if(!l[a]){if(!g[a]){var f="function"==typeof require&&require;if(!c&&f)return f(a,!0);if(b)return b(a,!0);f=Error("Cannot find module '"+a+"'");throw f.code="MODULE_NOT_FOUND", +f;}f=l[a]={exports:{}};g[a][0].call(f.exports,function(f){var c=g[a][1][f];return m(c?c:f)},f,f.exports,e,g,l,d)}return l[a].exports}for(var b="function"==typeof require&&require,c=0;ce?e-0:e+0)/b;e=k*r-n*a;x.y=(0>e?e-0:e+0)/b;return x}},{}],3:[function(e,g,l){g.exports.degree=e("./src/degree.js");g.exports.betweenness=e("./src/betweenness.js")},{"./src/betweenness.js":4,"./src/degree.js":5}],4:[function(e,g,l){g.exports=function(d,m){function b(a){r[a]/=2}function c(a){e[a.id]=0}function a(a){function c(a){a=a.id;-1===n[a]&&(n[a]=n[b]+1,p.push(a));n[a]===n[b]+1&&(k[a]+=k[b],h[a].push(b))}d.forEachNode(function(a){a=a.id;h[a]=[];n[a]=-1;k[a]=0}); +n[a]=0;k[a]=1;for(p.push(a);p.length;){var b=p.shift();Object.create(null);f.push(b);d.forEachLinkedNode(b,c,m)}}var p=[],f=[],h=Object.create(null),n=Object.create(null),k=Object.create(null),e=Object.create(null),g,r=Object.create(null);d.forEachNode(function(a){r[a.id]=0});d.forEachNode(function(b){g=b.id;a(g);for(d.forEachNode(c);f.length;){b=f.pop();for(var p=(1+e[b])/k[b],t=h[b],n=0;na)))return k.splice(a,1),0===k.length&&r.reset(),!0},addSpring:function(a,b,c,f,h){if(!a||!b)throw Error("Cannot add null spring to force simulator");"number"!==typeof c&&(c= +-1);a=new m(a,b,c,0<=h?h:-1,f);g.push(a);return a},removeSpring:function(a){if(a&&(a=g.indexOf(a),-1h&&(h=k.pos.x); +k.pos.yn&&(n=k.pos.y)}c.x1=b;c.x2=h;c.y1=f;c.y2=n}},reset:function(){c.x1=c.y1=0;c.x2=c.y2=0},getBestNewPosition:function(a){var p=0,f=0;if(a.length){for(var h=0;h +a.length?d.springLength:a.length,n=f.pos.x-c.pos.x,k=f.pos.y-c.pos.y,m=Math.sqrt(n*n+k*k);0===m&&(n=(b.nextDouble()-.5)/50,k=(b.nextDouble()-.5)/50,m=Math.sqrt(n*n+k*k));a=(!a.coeff||0>a.coeff?d.springCoeff:a.coeff)*(m-h)/m*a.weight;c.force.x+=a*n;c.force.y+=a*k;f.force.x-=a*n;f.force.y-=a*k}};c(d,m,["springCoeff","springLength"]);return m}},{"ngraph.expose":15,"ngraph.merge":24,"ngraph.random":25}],15:[function(e,g,l){function d(d,b,c){d.hasOwnProperty(c)&&"function"!==typeof b[c]&&(b[c]=function(a){return void 0!== +a?(d[c]=a,b):d[c]})}g.exports=function(m,b,c){if("[object Array]"===Object.prototype.toString.call(c))for(var a=0;ad&&(d=e);lk&&(k=l)}p=d-c;e=k-f;p>e?k=f+p:d=c+e;g=0;r=q();r.left=c;r.right=d;r.top=f;r.bottom=k;p=n-1;0n&&(d+=1,D=k,k=n,n+=n-D);v>l&&(d+=2,v=e,e=l,l+=l-v);(v=0===d?c.quad0:1===d?c.quad1: +2===d?c.quad2:3===d?c.quad3:null)?h.push(v,f):(v=q(),v.left=k,v.top=e,v.right=n,v.bottom=l,v.body=f,f=d,d=v,0===f?c.quad0=d:1===f?c.quad1=d:2===f?c.quad2=d:3===f&&(c.quad3=d))}},updateBodyForce:function(a){var b,c,d,h,k=0,e=0,g=1,C=0,q=1;for(f[0]=r;g;){b=f[C];var l=b.body,g=g-1,C=C+1;c=l!==a;l&&c?(c=l.pos.x-a.pos.x,d=l.pos.y-a.pos.y,h=Math.sqrt(c*c+d*d),0===h&&(c=(m.nextDouble()-.5)/50,d=(m.nextDouble()-.5)/50,h=Math.sqrt(c*c+d*d)),b=p*l.mass*a.mass/(h*h*h),k+=b*c,e+=b*d):c&&(c=b.massX/b.mass-a.pos.x, +d=b.massY/b.mass-a.pos.y,h=Math.sqrt(c*c+d*d),0===h&&(c=(m.nextDouble()-.5)/50,d=(m.nextDouble()-.5)/50,h=Math.sqrt(c*c+d*d)),(b.right-b.left)/hMath.abs(d.x-e.x)&&1E-8>b}},{}],20:[function(e,g,l){g.exports=function(){this.quad3=this.quad2=this.quad1=this.quad0=this.body=null;this.right=this.bottom=this.top=this.left=this.massY=this.massX=this.mass=0}},{}],21:[function(e,g,l){function d(b){return b}g.exports=function(b,c,a){c=c||d;a=a||d;b="string"===typeof b?JSON.parse(b):b;var p=m(),f;if(void 0===b.links||void 0===b.nodes)throw Error("Cannot load graph without links and nodes");for(f=0;fb)throw Error("Invalid number of nodes");var c=m(),a;for(a=0;ab)throw Error("At least two nodes are expected for complete graph");var c=m(),a,d;for(a=0;ab||0>c)throw Error("Graph dimensions are invalid. Number of nodes in each partition should be greater than 0");var a=m(),d,f;for(d=0;d +b)throw Error("Invalid number of nodes in balanced tree");var c=m(),a=Math.pow(2,b);0===b&&c.addNode(1);for(b=1;bb)throw Error("Invalid number of nodes");var c=m(),a;c.addNode(0);for(a=1;ab)throw Error("Invalid number of nodes");var c=d(b);c.addLink(0,b-1);c.addLink(b,2*b-1);return c},grid:function(b,c){if(1>b||1>c)throw Error("Invalid number of nodes in grid graph"); +var a=m(),d,f;if(1===b&&1===c)return a.addNode(0),a;for(d=0;db||1>c||1>a)throw Error("Invalid number of nodes in grid3 graph");var d=m(),f,h,n;if(1===b&&1===c&&1===a)return d.addNode(0),d;for(n=0;n +b)throw Error("Number of nodes shoul be >= 0");var c=m(),a;for(a=0;a=b)throw Error("Choose smaller `k`. It cannot be larger than number of nodes `n`");d=e("ngraph.random").random(d||42);var f=m(),h,n;for(h=0;hb)return!1;G();B.splice(b,1);var c=u[a.fromId],f=u[a.toId];c&&(b=d(a,c.links),0<=b&&c.links.splice(b,1));f&&(b=d(a,f.links),0<=b&&f.links.splice(b,1));M(a,"remove");L();return!0}function r(a,b){var c=u[a],d;if(!c)return null;for(d=0;d>>19)&4294967295;b=b+374761393+(b<<5)&4294967295;b=(b+3550635116^b<<9)&4294967295;b=b+4251993797+(b<<3)&4294967295;b=(b^3042594569^b>>>16)&4294967295;return(b&268435455)/268435456};return{next:function(a){return Math.floor(c()*a)},nextDouble:function(){return c()}}}g.exports= +{random:d,randomIterator:function(e,b){var c=b||d();if("function"!==typeof c.next)throw Error("customRandom does not match expected API: next() function is missing");return{forEach:function(a){var b,d,h;for(b=e.length-1;0m.indexOf(".")))throw Error("simplesvg currently does not support nested bindings");(a=d[m])?a.push(e):a=d[m]=[e]}}function b(a,b){function d(b){a.nodeValue=b[n]}var e=a.nodeValue;if(e&&(e=e.match(c))){var n=e[1];n.indexOf(".");(e=b[n])?e.push(d):e=b[n]=[d]}}g.exports=function(a){var b=Object.create(null);d(a,b);return{link:function(a){function c(b){b(a)} +Object.keys(b).forEach(function(a){b[a].forEach(c)})}}};var c=/{{(.+?)}}/},{}],30:[function(e,g,l){function d(){throw Error("DOMParser is not supported by this platform. Please open issue here https://github.com/anvaka/simplesvg");}e="undefined"===typeof DOMParser?{parseFromString:d}:new DOMParser;g.exports=e},{}],31:[function(e,g,l){function d(d,e,n,k){p=p||(document.addEventListener?{add:m,rm:b}:{add:c,rm:a});return p.add(d,e,n,k)}function m(a,b,c,d){a.addEventListener(b,c,d)}function b(a,b,c,d){a.removeEventListener(b, +c,d)}function c(a,b,c,d){if(d)throw Error("cannot useCapture in oldIE");a.attachEvent("on"+b,c)}function a(a,b,c,d){a.detachEvent("on"+b,c)}d.removeEventListener=function(d,e,n,k){p=p||(document.addEventListener?{add:m,rm:b}:{add:c,rm:a});return p.rm(d,e,n,k)};d.addEventListener=d;g.exports=d;var p=null},{}],32:[function(e,g,l){function d(a){a=c.betweenness(a);return b(a)}function m(a,d){var f=c.degree(a,d);return b(f)}function b(a){return Object.keys(a).sort(function(b,c){return a[c]-a[b]}).map(function(b){return{key:b, +value:a[b]}})}var c=e("ngraph.centrality");g.exports=function(){return{betweennessCentrality:d,degreeCentrality:m}}},{"ngraph.centrality":3}],33:[function(e,g,l){g.exports=function(){return{density:function(d,e){var b=d.getNodesCount();return 0===b?NaN:e?d.getLinksCount()/(b*(b-1)):2*d.getLinksCount()/(b*(b-1))}}}},{}],34:[function(e,g,l){g.exports=function(e,b){var c={};return{bindDragNDrop:function(a,e){var f;if(e){f=b.getNodeUI(a.id);f=d(f);if("function"===typeof e.onStart)f.onStart(e.onStart); +if("function"===typeof e.onDrag)f.onDrag(e.onDrag);if("function"===typeof e.onStop)f.onStop(e.onStop);c[a.id]=f}else if(f=c[a.id])f.release(),delete c[a.id]}}};var d=e("./dragndrop.js")},{"./dragndrop.js":35}],35:[function(e,g,l){g.exports=function(c){var a,e,f,h,n,k,g=0,q=0,l,x=!1,y=0,t=function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},z=function(a){t(a);return!1},A=function(a){a=a||window.event;var b=a.clientX,c=a.clientY;e&&e(a,{x:b-g,y:c-q});g=b;q=c},u=function(b){b=b||window.event; +if(x)return t(b),!1;if(1===b.button&&null!==window.event||0===b.button)return g=b.clientX,q=b.clientY,l=b.target||b.srcElement,a&&a(b,{x:g,y:q}),d.on("mousemove",A),d.on("mouseup",B),t(b),n=window.document.onselectstart,k=window.document.ondragstart,window.document.onselectstart=z,l.ondragstart=z,!1},B=function(a){a=a||window.event;d.off("mousemove",A);d.off("mouseup",B);window.document.onselectstart=n;l.ondragstart=k;l=null;f&&f(a)},F=function(a){if("function"===typeof h){a=a||window.event;a.preventDefault&& +a.preventDefault();a.returnValue=!1;var d,f=0,e=0;d=a||window.event;if(d.pageX||d.pageY)f=d.pageX,e=d.pageY;else if(d.clientX||d.clientY)f=d.clientX+window.document.body.scrollLeft+window.document.documentElement.scrollLeft,e=d.clientY+window.document.body.scrollTop+window.document.documentElement.scrollTop;d=[f,e];f=b(c);h(a,a.wheelDelta?a.wheelDelta/360:a.detail/-9,{x:d[0]-f[0],y:d[1]-f[1]})}},H=function(a){!h&&a?"webkit"===m.browser?c.addEventListener("mousewheel",F,!1):c.addEventListener("DOMMouseScroll", +F,!1):h&&!a&&("webkit"===m.browser?c.removeEventListener("mousewheel",F,!1):c.removeEventListener("DOMMouseScroll",F,!1));h=a},D=function(a,b){return(a.clientX-b.clientX)*(a.clientX-b.clientX)+(a.clientY-b.clientY)*(a.clientY-b.clientY)},v=function(a){if(1===a.touches.length){t(a);var b=a.touches[0],c=b.clientX,b=b.clientY;e&&e(a,{x:c-g,y:b-q});g=c;q=b}else 2===a.touches.length&&(c=D(a.touches[0],a.touches[1]),b=0,cy&&(b=1),h(a,b,{x:a.touches[0].clientX,y:a.touches[0].clientY}),y=c,t(a), +a.preventDefault&&a.preventDefault())},w=function(a){x=!1;d.off("touchmove",v);d.off("touchend",w);d.off("touchcancel",w);l=null;f&&f(a)},E=function(b){if(1===b.touches.length){var c=b.touches[0];t(b);b.preventDefault&&b.preventDefault();g=c.clientX;q=c.clientY;l=b.target||b.srcElement;a&&a(b,{x:g,y:q});x||(x=!0,d.on("touchmove",v),d.on("touchend",w),d.on("touchcancel",w))}else 2===b.touches.length&&(t(b),b.preventDefault&&b.preventDefault(),y=D(b.touches[0],b.touches[1]))};c.addEventListener("mousedown", +u);c.addEventListener("touchstart",E);return{onStart:function(b){a=b;return this},onDrag:function(a){e=a;return this},onStop:function(a){f=a;return this},onScroll:function(a){H(a);return this},release:function(){c.removeEventListener("mousedown",u);c.removeEventListener("touchstart",E);d.off("mousemove",A);d.off("mouseup",B);d.off("touchmove",v);d.off("touchend",w);d.off("touchcancel",w);H(null)}}};var d=e("../Utils/documentEvents.js"),m=e("../Utils/browserInfo.js"),b=e("../Utils/findElementPosition.js")}, +{"../Utils/browserInfo.js":39,"../Utils/documentEvents.js":40,"../Utils/findElementPosition.js":41}],36:[function(e,g,l){g.exports=function(e,b){var c=d(b),a=null,g={},f={x:0,y:0};c.mouseDown(function(b,d){a=b;f.x=d.clientX;f.y=d.clientY;c.mouseCapture(a);var e=g[b.id];if(e&&e.onStart)e.onStart(d,f);return!0}).mouseUp(function(b){c.releaseMouseCapture(a);a=null;if((b=g[b.id])&&b.onStop)b.onStop();return!0}).mouseMove(function(b,c){if(a){var d=g[a.id];if(d&&d.onDrag)d.onDrag(c,{x:c.clientX-f.x,y:c.clientY- +f.y});f.x=c.clientX;f.y=c.clientY;return!0}});return{bindDragNDrop:function(a,b){(g[a.id]=b)||delete g[a.id]}}};var d=e("../WebGL/webglInputEvents.js")},{"../WebGL/webglInputEvents.js":57}],37:[function(e,g,l){g.exports=function(c,a){a=d(a,{maxX:1024,maxY:1024,seed:"Deterministic randomness made me do this"});var e=m(a.seed),f=new b(Number.MAX_VALUE,Number.MAX_VALUE,Number.MIN_VALUE,Number.MIN_VALUE),h={},g=function(b){return{x:e.next(a.maxX),y:e.next(a.maxY)}},k="function"===typeof Object.create? +Object.create(null):{},l=function(a){k[a.id]=g(a);a=k[a.id];a.xf.x2&&(f.x2=a.x);a.yf.y2&&(f.y2=a.y)},q=function(){0!==c.getNodesCount()&&(f.x1=Number.MAX_VALUE,f.y1=Number.MAX_VALUE,f.x2=Number.MIN_VALUE,f.y2=Number.MIN_VALUE,c.forEachNode(l))},r=function(a){h[a.id]=a},x=function(a){for(var b=0;be.indexOf("compatible")&&m.exec(e)||[];e={browser:e[1]||"",version:e[2]||"0"}}else e={browser:"",version:"0"};g.exports=e},{}],40:[function(e,g,l){e("./nullEvents.js");g.exports={on:function(d,e){document.addEventListener(d,e)},off:function(d,e){document.removeEventListener(d, +e)}}},{"./nullEvents.js":44}],41:[function(e,g,l){g.exports=function(d){var e=0,b=0;if(d.offsetParent){do e+=d.offsetLeft,b+=d.offsetTop;while(null!==(d=d.offsetParent))}return[e,b]}},{}],42:[function(e,g,l){g.exports=function(d){if(!d)throw{message:"Cannot get dimensions of undefined container"};return{left:0,top:0,width:d.clientWidth,height:d.clientHeight}}},{}],43:[function(e,g,l){var d=e("gintersect");g.exports=function(e,b,c,a,g,f,h,n){return d(e,b,e,a,g,f,h,n)||d(e,a,c,a,g,f,h,n)||d(c,a,c,b, +g,f,h,n)||d(c,b,e,b,g,f,h,n)}},{gintersect:2}],44:[function(e,g,l){function d(){}g.exports={on:d,off:d,stop:d}},{}],45:[function(e,g,l){g.exports=function(d,e,b,c){this.x1=d||0;this.y1=e||0;this.x2=b||0;this.y2=c||0}},{}],46:[function(e,g,l){(function(d){function e(){}g.exports=function(){function b(b){var c=(new Date).getTime(),d=Math.max(0,16-(c-a)),e=h.setTimeout(function(){b(c+d)},d);a=c+d;return e}function c(a){h.clearTimeout(a)}var a=0,g=["ms","moz","webkit","o"],f,h;h="undefined"!==typeof window? +window:"undefined"!==typeof d?d:{setTimeout:e,clearTimeout:e};for(f=0;fb,c)}));e.forEachNode(A);e.off("changed",u);e.on("changed",u)}function D(){L=!1;e.off("changed",u);N&&(N.release(),N=null);c.off("resize",B);R.off();G.stop();e.forEachLink(function(a){k.renderLinks&&w.releaseLink(a)});e.forEachNode(function(a){I.bindDragNDrop(a,null);w.releaseNode(a)});v.dispose();w.release(E)}k=k||{};var v=k.layout,w=k.graphics,E=k.container,M=void 0!==k.interactive?k.interactive:!0,I,G,L= +!1,K=!0,O=!1,P=!1,Q=!1,J={offsetX:0,offsetY:0,scale:1},R=d({}),N;return{run:function(c){if(!L){E=E||window.document.body;v=v||m(e,{springLength:80,springCoeff:2E-4});w=w||b(e,{container:E});k.hasOwnProperty("renderLinks")||(k.renderLinks=!0);k.prerender=k.prerender||0;I=(w.inputManager||a)(e,w);if("number"===typeof k.prerender&&0a.id&&(a=a.id,c=B[b],B[b]=B[a],B[b].id=b,B[a]=c,B[a].id=a)},graphCenterChanged:function(a,b){t[12]=2*a/q-1;t[13]=1-2*b/r;I()},addLink:function(a,b){var c=y++,d=M(a);d.id=c;d.pos=b;v.createLink(d);B[c]=d;return D[a.id]=d},addNode:function(a,b){var c=x++,d=E(a);d.id=c;d.position=b;d.node=a;w.createNode(d);u[c]=d;return H[a.id]=d},translateRel:function(a,b){t[12]+=2*t[0]*a/q/t[0];t[13]-=2*t[5]*b/r/t[5];I()},scale:function(a,b){var c=2*b.x/q-1,d=1-2*b.y/r,c=c- +t[12],d=d-t[13];t[12]+=c*(1-a);t[13]+=d*(1-a);t[0]*=a;t[5]*=a;I();this.fire("rescaled");return t[0]},resetScale:function(){G();l&&(L(),I());return this},init:function(a){var b={};e.preserveDrawingBuffer&&(b.preserveDrawingBuffer=!0);g=a;L();G();g.appendChild(k);l=k.getContext("experimental-webgl",b);if(!l)throw window.alert("Could not initialize WebGL. Seems like the browser doesn't support it."),"Could not initialize WebGL. Seems like the browser doesn't support it.";e.enableBlending&&(l.blendFunc(l.SRC_ALPHA, +l.ONE_MINUS_SRC_ALPHA),l.enable(l.BLEND));e.clearColor&&(a=e.clearColorValue,l.clearColor(a.r,a.g,a.b,a.a),this.beginRender=function(){l.clear(l.COLOR_BUFFER_BIT)});v.load(l);v.updateSize(q/2,r/2);w.load(l);w.updateSize(q/2,r/2);I();"function"===typeof F&&F(k)},release:function(a){k&&a&&a.removeChild(k)},isSupported:function(){var a=window.document.createElement("canvas");return a&&a.getContext&&a.getContext("experimental-webgl")},releaseLink:function(a){0a.length?(b=new Float32Array(a.length*c*2),b.set(a),b):a},copyArrayPart:d,swapArrayPart:m,getLocations:function(a,c){for(var d= +{},e=0;e= +q.length){var l=new d(g*f);q.push(l)}l=q[c.textureNumber];l.ctx.drawImage(k,c.col*f,c.row*f,f,f);r[m]=k.src;n[k.src]=h;l.isDirty=!0;e(h)};k.src=c}}};return x}},{"./texture.js":52}],55:[function(e,g,l){g.exports=function(d,e){return{_texture:0,_offset:0,size:"number"===typeof d?d:32,src:e}}},{}],56:[function(e,g,l){var d=e("./webglAtlas.js"),m=e("./webgl.js");g.exports=function(){var b,c,a,e,f,h,g=0,k=new Float32Array(64),l,q,r,x;return{load:function(g){a=g;f=m(g);b=new d(1024);c=f.createProgram("attribute vec2 a_vertexPos;\nattribute float a_customAttributes;\nuniform vec2 u_screenSize;\nuniform mat4 u_transform;\nuniform float u_tilesPerTexture;\nvarying vec3 vTextureCoord;\nvoid main(void) {\n gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0, 1);\nfloat corner = mod(a_customAttributes, 4.);\nfloat tileIndex = mod(floor(a_customAttributes / 4.), u_tilesPerTexture);\nfloat tilesPerRow = sqrt(u_tilesPerTexture);\nfloat tileSize = 1./tilesPerRow;\nfloat tileColumn = mod(tileIndex, tilesPerRow);\nfloat tileRow = floor(tileIndex/tilesPerRow);\nif(corner == 0.0) {\n vTextureCoord.xy = vec2(0, 1);\n} else if(corner == 1.0) {\n vTextureCoord.xy = vec2(1, 1);\n} else if(corner == 2.0) {\n vTextureCoord.xy = vec2(0, 0);\n} else {\n vTextureCoord.xy = vec2(1, 0);\n}\nvTextureCoord *= tileSize;\nvTextureCoord.x += tileColumn * tileSize;\nvTextureCoord.y += tileRow * tileSize;\nvTextureCoord.z = floor(floor(a_customAttributes / 4.)/u_tilesPerTexture);\n}", +"precision mediump float;\nvarying vec4 color;\nvarying vec3 vTextureCoord;\nuniform sampler2D u_sampler0;\nuniform sampler2D u_sampler1;\nuniform sampler2D u_sampler2;\nuniform sampler2D u_sampler3;\nvoid main(void) {\n if (vTextureCoord.z == 0.) {\n gl_FragColor = texture2D(u_sampler0, vTextureCoord.xy);\n } else if (vTextureCoord.z == 1.) {\n gl_FragColor = texture2D(u_sampler1, vTextureCoord.xy);\n } else if (vTextureCoord.z == 2.) {\n gl_FragColor = texture2D(u_sampler2, vTextureCoord.xy);\n } else if (vTextureCoord.z == 3.) {\n gl_FragColor = texture2D(u_sampler3, vTextureCoord.xy);\n } else { gl_FragColor = vec4(0, 1, 0, 1); }\n}"); +a.useProgram(c);h=f.getLocations(c,"a_vertexPos a_customAttributes u_screenSize u_transform u_sampler0 u_sampler1 u_sampler2 u_sampler3 u_tilesPerTexture".split(" "));a.uniform1f(h.tilesPerTexture,1024);a.enableVertexAttribArray(h.vertexPos);a.enableVertexAttribArray(h.customAttributes);e=a.createBuffer()},position:function(a,b){var c=18*a.id;k[c]=b.x-a.size;k[c+1]=b.y-a.size;k[c+2]=4*a._offset;k[c+3]=b.x+a.size;k[c+4]=b.y-a.size;k[c+5]=4*a._offset+1;k[c+6]=b.x-a.size;k[c+7]=b.y+a.size;k[c+8]=4*a._offset+ +2;k[c+9]=b.x-a.size;k[c+10]=b.y+a.size;k[c+11]=4*a._offset+2;k[c+12]=b.x+a.size;k[c+13]=b.y-a.size;k[c+14]=4*a._offset+1;k[c+15]=b.x+a.size;k[c+16]=b.y+a.size;k[c+17]=4*a._offset+3},createNode:function(a){k=f.extendArray(k,g,18);g+=1;var c=b.getCoordinates(a.src);c?a._offset=c.offset:(a._offset=0,b.load(a.src,function(b){a._offset=b.offset}))},removeNode:function(a){0d-H&&f[0]===z?g(x,f):g(r,f),H=d,g(C,f)&&c(a))})})(e.getGraphicsRoot());var z={mouseEnter:function(a){"function"=== +typeof a&&h.push(a);return z},mouseLeave:function(a){"function"===typeof a&&l.push(a);return z},mouseDown:function(a){"function"===typeof a&&k.push(a);return z},mouseUp:function(a){"function"===typeof a&&C.push(a);return z},mouseMove:function(a){"function"===typeof a&&q.push(a);return z},click:function(a){"function"===typeof a&&r.push(a);return z},dblClick:function(a){"function"===typeof a&&x.push(a);return z},mouseCapture:function(a){f=a},releaseMouseCapture:function(){f=null}};return e.webglInputEvents= +z}},{"../Utils/documentEvents.js":40}],58:[function(e,g,l){var d=e("./parseColor.js");g.exports=function(e){return{color:d(e)}}},{"./parseColor.js":51}],59:[function(e,g,l){var d=e("./webgl.js");g.exports=function(){var e=2*(2*Float32Array.BYTES_PER_ELEMENT+Uint32Array.BYTES_PER_ELEMENT),b,c,a,g,f,h=0,l,k=new ArrayBuffer(16*e),C=new Float32Array(k),q=new Uint32Array(k),r,x,y,t;return{load:function(e){c=e;g=d(e);b=g.createProgram("attribute vec2 a_vertexPos;\nattribute vec4 a_color;\nuniform vec2 u_screenSize;\nuniform mat4 u_transform;\nvarying vec4 color;\nvoid main(void) {\n gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0.0, 1.0);\n color = a_color.abgr;\n}", +"precision mediump float;\nvarying vec4 color;\nvoid main(void) {\n gl_FragColor = color;\n}");c.useProgram(b);f=g.getLocations(b,["a_vertexPos","a_color","u_screenSize","u_transform"]);c.enableVertexAttribArray(f.vertexPos);c.enableVertexAttribArray(f.color);a=c.createBuffer()},position:function(a,b,c){var d=6*a.id;C[d]=b.x;C[d+1]=b.y;q[d+2]=a.color;C[d+3]=c.x;C[d+4]=c.y;q[d+5]=a.color},createLink:function(a){if((h+1)*e>k.byteLength){var b=new ArrayBuffer(2*k.byteLength),c=new Float32Array(b), +d=new Uint32Array(b);d.set(q);C=c;q=d;k=b}h+=1;l=a.id},removeLink:function(a){0a.id&&g.swapArrayPart(C,6*a.id,6*l,6);0=h.byteLength){var a=new ArrayBuffer(2*h.byteLength),b=new Float32Array(a),c=new Uint32Array(a);c.set(k);l=b;k=c;h=a}C+=1},replaceProperties:function(){},render:function(){c.useProgram(b);c.bindBuffer(c.ARRAY_BUFFER,a);c.bufferData(c.ARRAY_BUFFER,h,c.DYNAMIC_DRAW);y&&(y=!1,c.uniformMatrix4fv(g.transform,!1,x),c.uniform2f(g.screenSize,q,r));c.vertexAttribPointer(g.vertexPos,3,c.FLOAT,!1,4*Float32Array.BYTES_PER_ELEMENT,0);c.vertexAttribPointer(g.color, +4,c.UNSIGNED_BYTE,!0,4*Float32Array.BYTES_PER_ELEMENT,12);c.drawArrays(c.POINTS,0,C)}}}},{"./webgl.js":53}],61:[function(e,g,l){var d=e("./parseColor.js");g.exports=function(e,b){return{size:"number"===typeof e?e:10,color:d(b)}}},{"./parseColor.js":51}],62:[function(e,g,l){g.exports="0.8.1"},{}]},{},[1])(1)}); diff --git a/datafiles/static/hackage.css b/datafiles/static/hackage.css index c5ac4417d..f16f40825 100644 --- a/datafiles/static/hackage.css +++ b/datafiles/static/hackage.css @@ -9,9 +9,19 @@ html { height: 100%; } +@media (prefers-color-scheme: dark) { + body { + background: #333; + color: #fefefe; + } +} +@media (prefers-color-scheme: light) { + body { + background: #fefefe; + color: #333; + } +} body { - background: #fefefe; - color: #333; text-align: left; min-height: 100vh; position: relative; @@ -59,8 +69,16 @@ dd { } a { text-decoration: none; } -a[href]:link { color: #9E358F; } -a[href]:visited {color: #6F5F9C; } + +@media (prefers-color-scheme: dark) { + a[href]:link { color: #EB82DC; } /* 30% brighter */ + a[href]:visited { color: #D5C5FF; } /* 40% brighter */ +} +@media (prefers-color-scheme: light) { + a[href]:link { color: #9E358F; } + a[href]:visited {color: #6F5F9C; } +} + a[href]:hover { text-decoration:underline; } /* @end */ @@ -142,9 +160,19 @@ pre, code, kbd, samp, .src { /* @group Common */ +@media (prefers-color-scheme: dark) { + .caption, h1, h2, h3, h4, h5, h6 { + color: #5E5184; + filter: brightness(2.0); + } +} +@media (prefers-color-scheme: light) { + .caption, h1, h2, h3, h4, h5, h6 { + color: #5E5184; + } +} .caption, h1, h2, h3, h4, h5, h6 { font-weight: bold; - color: #5E5184; margin: 1.33em 0 0.7em 0; line-height: 1.05em; } @@ -194,10 +222,19 @@ ul.links li a, ul.links li form { cursor: pointer; } +@media (prefers-color-scheme: dark) { + pre { + background-color: #474747; /* 20% brighter than background */ + } +} +@media (prefers-color-scheme: light) { + pre { + background-color: #f7f7f7; + } +} pre { padding: 0.5rem 1rem; margin: 1em 0; - background-color: #f7f7f7; overflow: auto; } @@ -402,7 +439,6 @@ table.properties td, table.properties th { } div #properties { - background: #fefefe; width: 40%; margin-bottom: 2em; } @@ -583,11 +619,21 @@ div#style-menu-holder { display: block; } +@media (prefers-color-scheme: dark) { + #footer { + background: #222; + color: #ededed; + } +} +@media (prefers-color-scheme: light) { + #footer { + background: #ededed; + color: #222; + } +} #footer { - background: #ededed; border-top: 1px solid #aaa; padding: 0.5em 0; - color: #222; text-align: center; width: 100%; height: 3em; @@ -625,8 +671,17 @@ ul.links li form button { /* @group Front Matter */ +@media (prefers-color-scheme: dark) { + #table-of-contents { + background: #222; + } +} +@media (prefers-color-scheme: light) { + #table-of-contents { + background: #f7f7f7; + } +} #table-of-contents { - background: #f7f7f7; padding: 1em; margin: 0; margin-top: 1em; @@ -986,15 +1041,30 @@ table.fancy th { background: #f0f0f0; } +@media (prefers-color-scheme: dark) { + table.fancy td, table.properties td, + table.fancy th, table.properties th { + background: #333; + } + table.dataTable.compact.fancy tbody th, + table.dataTable.compact.fancy tbody td { + background: #333; + } +} + table.fancy td, table.properties td, table.fancy th, table.properties th { padding: 0.15em 0.45em; } +table.fancy tr.even td { + background-color: #eee; +} + table.dataTable.compact.fancy tbody th, table.dataTable.compact.fancy tbody td { - padding: 6px 10px; - line-height: normal; + padding: 6px 10px; + line-height: normal; } ul.searchresults li { @@ -1042,6 +1112,106 @@ a.deprecated[href]:visited { color: #61B01E; } +/* Paginator */ +#paginatorContainer { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: space-between; +} + +#paginatorContainer > div { + padding: 1em 0; +} + +.paginator { + display: flex; + flex-wrap: wrap; +} + +/* Styles Next/Prev when they have no href */ + +@media (prefers-color-scheme: dark) { + .paginator a { + color: #474747; + } +} + +@media (prefers-color-scheme: light) { + .paginator a { + color: #666 !important; + } +} + +.paginator a { + cursor: default; + background: none; + border: none; + padding: 0.5em 1em; + text-decoration: none; +} + +.paginator span { + color: #333; + padding: 0.5em 1em; +} + + +@media (prefers-color-scheme: dark) { + .paginator a:link:hover, .paginator a:visited:hover { + color: #474747; + } +} + +@media (prefers-color-scheme: light) { + .paginator a:link:hover, .paginator a:visited:hover { + color: #333 !important; + } +} + +.paginator a:link, .paginator a:visited { + border: 1px solid transparent; + border-radius: 2px; +} + +@media (prefers-color-scheme: dark) { + .paginator a:link:hover, .paginator a:visited:hover { + color: #585858; + background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); + } +} + +@media (prefers-color-scheme: light) { + .paginator a:link:hover, .paginator a:visited:hover { + color: white !important; + background: linear-gradient(to bottom, #585858 0%, #111 100%); + } +} + +.paginator a:link:hover, .paginator a:visited:hover { + border: 1px solid #111; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + .paginator .current, .paginator .current:hover { + color: #474747; + background: linear-gradient(to bottom, #585858 0%, #111 100%); + } +} + +@media (prefers-color-scheme: light) { + .paginator .current, .paginator .current:hover { + color: #666 !important; + background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); + } +} + +.paginator .current, +.paginator .current:hover { + border: 1px solid #979797; +} + p.registration-email { border: 1px solid rgb(196,69,29); padding: 0 1em; diff --git a/datafiles/templates/Html/browse.html.st b/datafiles/templates/Html/browse.html.st index a7b85a496..a8b8e6afa 100644 --- a/datafiles/templates/Html/browse.html.st +++ b/datafiles/templates/Html/browse.html.st @@ -51,38 +51,6 @@ #browseTable th.descending { background-image: url(/static/images/sort_desc.png); } - .paginator { - margin-left: auto; - } - .paginator a { - box-sizing: border-box; - display: inline-block; - min-width: 1.5em; - padding: 0.5em 1em; - margin-left: 2px; - text-align: center; - text-decoration: none !important; - color: #333 !important; - border: 1px solid transparent; - border-radius: 2px; - } - .paginator .current, .paginator .current:hover { - color: #333 !important; - border: 1px solid #979797; - background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); - } - .paginator a:hover { - color: white !important; - border: 1px solid #111; - background: linear-gradient(to bottom, #585858 0%, #111 100%); - } - .paginator span { - padding: 0 1em; - cursor: default; - } - .paginator .disabled { - color: #666; - } .filterSuggestion { display: flex; align-items: center; @@ -102,11 +70,6 @@ .filterSuggestion > div > input { margin: 0; } - #paginatorContainer { - display: flex; - align-items: center; - flex-wrap: wrap; - } #fatalError { display: none; color: red; @@ -210,7 +173,7 @@
Only show packages with more than 1000 downloads within the last 30 days. The download count is inexact because Hackage uses a content delivery network.
(lastUpload < 2021-10-29)
Only show packages for which the last upload was before (i.e. excluding) the given UTC date in the 'complete date' format as specified using ISO 8601.
-
(lastUpload = 2021-10-29)
+
(lastUpload = 2021-10-29)
x
Only show packages for which the last upload was within the 24 hours of the given UTC date.
(maintainer:SimonMarlow)
Only show packages for which the maintainers list includes the user name SimonMarlow.
@@ -249,6 +212,7 @@ Description Tags Last U/L + Last Version Maintainers diff --git a/datafiles/templates/Html/candidate-page.html.st b/datafiles/templates/Html/candidate-page.html.st index 196b8dd03..412591585 100644 --- a/datafiles/templates/Html/candidate-page.html.st +++ b/datafiles/templates/Html/candidate-page.html.st @@ -1,6 +1,9 @@ + $if(doc.hasQuickNavV1)$ + + $endif$ $hackageCssTheme()$ $package.name$$if(package.optional.hasSynopsis)$: $package.optional.synopsis$$endif$ @@ -130,6 +133,12 @@ $downloadSection$ <h4>Maintainer's Corner</h4> + <p>Package maintainers</p> + <ul> + <li> + $maintainers$ + </li> + </ul> <p>For package maintainers and hackage trustees</p> <ul> <li> @@ -146,5 +155,10 @@ $package.optional.readme$ $endif$ + $if(doc.hasQuickNavV1)$ + <script src="$doc.baseUrl$/quick-jump.min.js" type="text/javascript"></script> + <script type="text/javascript"> quickNav.init("$doc.baseUrl$", function(toggle) {var t = document.getElementById('quickjump-trigger');if (t) {t.onclick = function(e) { e.preventDefault(); toggle(); };}}); </script> + $endif$ + </body> </html> diff --git a/datafiles/templates/Html/graph.html.st b/datafiles/templates/Html/graph.html.st new file mode 100644 index 000000000..205d1680c --- /dev/null +++ b/datafiles/templates/Html/graph.html.st @@ -0,0 +1,299 @@ +<!DOCTYPE html> +<html> + + <head> + <title>Hackage Dependencies Graph + + ` + + + + + + +
+
+ +
+ +
+
+
+
+ + + + + + + + + + + diff --git a/datafiles/templates/Html/maintain.html.st b/datafiles/templates/Html/maintain.html.st index 5b7500b00..256e69be1 100644 --- a/datafiles/templates/Html/maintain.html.st +++ b/datafiles/templates/Html/maintain.html.st @@ -46,6 +46,11 @@ package after its been released.

$versions:{pkgid|$pkgid$}; separator=", "$

+
Test settings
+
If your package contains tests that can't run on hackage, you can disable them here. +

$versions:{pkgid|$pkgid$}; separator=", "$

+
+
Trigger rebuild
Reset the fail count and trigger rebuild. Choose this option only if you believe our build process didn't go right for some reason. Reseting fail count won't trigger rebuild if your package has documentation.

$versions:{pkgid|$pkgid$}; separator=", "$

diff --git a/datafiles/templates/Html/package-page.html.st b/datafiles/templates/Html/package-page.html.st index ade94df4e..2adb3881f 100644 --- a/datafiles/templates/Html/package-page.html.st +++ b/datafiles/templates/Html/package-page.html.st @@ -36,9 +36,15 @@ $if(isDeprecated)$
- Deprecated. + Deprecated. $deprecatedMsg$
+ $else$ + $if(isDeprecatedVersion)$ +
+ This version is deprecated. +
+ $endif$ $endif$
@@ -87,7 +93,13 @@ diff --git a/datafiles/templates/Html/reports-test.html.st b/datafiles/templates/Html/reports-test.html.st new file mode 100644 index 000000000..72a4eaee1 --- /dev/null +++ b/datafiles/templates/Html/reports-test.html.st @@ -0,0 +1,25 @@ + + + +$hackageCssTheme()$ +Test settings + + +$hackagePageHeader()$ + +
+

Test settings for $pkgid$

+ +
+ +
+
Run tests
+
+ Whether hackage should run the tests. +
+ +

+

+ +
+ diff --git a/datafiles/templates/Html/table-interface.html.st b/datafiles/templates/Html/table-interface.html.st index 7eeada219..f08fb67eb 100644 --- a/datafiles/templates/Html/table-interface.html.st +++ b/datafiles/templates/Html/table-interface.html.st @@ -24,10 +24,12 @@
Name
DLs
Rating
+
Rev Deps
Description
Tags
Last U/L
-
Maintainer
+
Last Version
+
Maintainers
diff --git a/datafiles/templates/UserNotify/user-notify-form.html.st b/datafiles/templates/UserNotify/user-notify-form.html.st new file mode 100644 index 000000000..989af5a92 --- /dev/null +++ b/datafiles/templates/UserNotify/user-notify-form.html.st @@ -0,0 +1,52 @@ + + + +$hackageCssTheme()$ +Set user notification preferences | Hackage + + + +$hackagePageHeader()$ + +
+

Change notification preferences

+ +$if(showConfirmationOfSave)$ +

+ Notification preferences saved! The updated preferences are shown below. +

+$endif$ + +
+ + + + + + + + + + +
$notifyEnabled$ + +
$notifyRevisionRange$ + +
$notifyUpload$ + +
$notifyMaintainerGroup$ + +
$notifyDocBuilderReport$ + +
$notifyPendingTags$ +
+ +
+
+ diff --git a/datafiles/templates/UserSignupReset/SignupRequest.html.st b/datafiles/templates/UserSignupReset/SignupRequest.html.st index 4bf1be3f3..21b3265f7 100644 --- a/datafiles/templates/UserSignupReset/SignupRequest.html.st +++ b/datafiles/templates/UserSignupReset/SignupRequest.html.st @@ -35,6 +35,14 @@ Organizational, rather than individual, accounts are also permitted. e.g. jnovak@example.com (but do not use the style "Jan Novák" <jnovak@example.com>) + + + + +captcha +The captcha will expire in 10 minutes. Click on the image to change one. + +

Your email address will be used to confirm your account (and if you ever @@ -97,4 +105,27 @@ service.

+ diff --git a/datafiles/templates/Users/manage.html.st b/datafiles/templates/Users/manage.html.st index 0f52f342e..9ebb91ba5 100644 --- a/datafiles/templates/Users/manage.html.st +++ b/datafiles/templates/Users/manage.html.st @@ -15,6 +15,9 @@ $hackagePageHeader(deauthUser="1")$

Change full name or e-mail address

You can change your full name or e-mail address.

+

Change notification preferences

+

You can change your notification preferences.

+

Authentication Tokens

You can register API authentication token to use them to for example have services like continuous integration upload packages on your behalf without providing them your username and/or password. diff --git a/datafiles/templates/accounts.html.st b/datafiles/templates/accounts.html.st index 8b4b79d5d..d1a8c6191 100644 --- a/datafiles/templates/accounts.html.st +++ b/datafiles/templates/accounts.html.st @@ -27,8 +27,9 @@ maintainer group.

Account Management

-

You can modify various settings for your account, including changing - the password, as well as creating authentication +You can modify various settings for your account, including changing + the email address and password, setting email notification + preferences, as well as creating authentication tokens, at the account management page.

diff --git a/datafiles/templates/index.html.st b/datafiles/templates/index.html.st index 5af98f2d6..71ea40320 100644 --- a/datafiles/templates/index.html.st +++ b/datafiles/templates/index.html.st @@ -4,6 +4,21 @@ $hackageCssTheme()$ Introduction | Hackage + diff --git a/exes/BuildClient.hs b/exes/BuildClient.hs index f72d6e251..cc9f593fb 100644 --- a/exes/BuildClient.hs +++ b/exes/BuildClient.hs @@ -38,7 +38,7 @@ import System.Exit(exitFailure, ExitCode(..)) import System.FilePath import System.Directory (canonicalizePath, createDirectoryIfMissing, doesFileExist, doesDirectoryExist, getDirectoryContents, - renameFile, removeFile, getAppUserDataDirectory, + renameFile, removeFile, createDirectory, removeDirectoryRecursive, createDirectoryIfMissing, makeAbsolute) import System.Console.GetOpt @@ -156,13 +156,24 @@ initialise opts uri auxUris readMissingOpt prompt = maybe (putStrLn prompt >> getLine) return -- | Parse the @00-index.cache@ file of the available package repositories. -parseRepositoryIndices :: Verbosity -> IO (M.Map PackageIdentifier Tar.EpochTime) -parseRepositoryIndices verbosity = do - cabalDir <- getAppUserDataDirectory "cabal/packages" +parseRepositoryIndices :: BuildOpts -> Verbosity -> IO (M.Map PackageIdentifier Tar.EpochTime) +parseRepositoryIndices opts verbosity = do cacheDirs <- listDirectory cabalDir - indexFiles <- filterM doesFileExist $ map (\dir -> cabalDir dir "01-index.tar") cacheDirs + indexFiles <- catMaybes <$> mapM findIdx cacheDirs M.unions <$> mapM readIndex indexFiles where + cabalDir = bo_stateDir opts "cached-tarballs" + findIdx dir = do + let index01 = cabalDir dir "01-index.tar" + index00 = cabalDir dir "00-index.tar" + b <- doesFileExist index01 + if b + then return (Just index01) + else do + b2 <- doesFileExist index00 + if b2 + then return (Just index00) + else return Nothing readIndex fname = do bs <- BS.readFile fname let mkPkg pkg entry = (pkg, Tar.entryTime entry) @@ -362,7 +373,9 @@ data DocInfo = DocInfo { docInfoPackage :: PackageIdentifier , docInfoHasDocs :: HasDocs , docInfoIsCandidate :: Bool + , docInfoRunTests :: Bool } + deriving Show docInfoPackageName :: DocInfo -> PackageName docInfoPackageName = pkgName . docInfoPackage @@ -410,8 +423,8 @@ getDocumentationStats verbosity opts config pkgs = do (Just (perrs, packages), Just (cerrs, candidates)) -> do liftIO . when (not . null $ perrs) . putStrLn $ "failed package json parses: " ++ show perrs liftIO . when (not . null $ cerrs) . putStrLn $ "failed candidate json parses: " ++ show cerrs - packages' <- liftIO $ mapM checkFailed packages - candidates' <- liftIO $ mapM checkFailed candidates + let packages' = map checkFailed packages + candidates' = map checkFailed candidates return $ map (setIsCandidate False) packages' ++ map (setIsCandidate True) candidates' where @@ -447,21 +460,23 @@ getDocumentationStats verbosity opts config pkgs = do addEnd (Just pkgs') Nothing uri = uri "docs.json" ++ "?pkgs=" ++ (getQry pkgs') addEnd Nothing Nothing uri = uri "docs.json" - checkFailed :: BR.PkgDetails -> IO (PackageIdentifier, HasDocs) - checkFailed pkgDetails = do + checkFailed :: BR.PkgDetails -> (PackageIdentifier, HasDocs, Bool) + checkFailed pkgDetails = let pkgId = BR.pkid pkgDetails - case (BR.docs pkgDetails, BR.failCnt pkgDetails) of - (True , _) -> return (pkgId, HasDocs) - (False, Just BR.BuildOK) -> return (pkgId, DocsFailed) - (False, Just (BR.BuildFailCnt a)) - | a >= bo_buildAttempts opts -> return (pkgId, DocsFailed) - (False, _) -> return (pkgId, DocsNotBuilt) - - setIsCandidate :: Bool -> (PackageIdentifier, HasDocs) -> DocInfo - setIsCandidate isCandidate (pId, hasDocs) = DocInfo { + hasDocs = case (BR.docs pkgDetails, BR.failCnt pkgDetails) of + (True , _) -> HasDocs + (False, Just BR.BuildOK) -> DocsFailed + (False, Just (BR.BuildFailCnt a)) + | a >= bo_buildAttempts opts -> DocsFailed + (False, _) -> DocsNotBuilt + in (pkgId, hasDocs, fromMaybe True $ BR.runTests pkgDetails) + + setIsCandidate :: Bool -> (PackageIdentifier, HasDocs, Bool) -> DocInfo + setIsCandidate isCandidate (pId, hasDocs, runTests) = DocInfo { docInfoPackage = pId , docInfoHasDocs = hasDocs , docInfoIsCandidate = isCandidate + , docInfoRunTests = runTests } @@ -474,12 +489,15 @@ buildOnce opts pkgs = keepGoing $ do config <- readConfig opts notice verbosity "Initialising" + handleDoesNotExist () $ + removeDirectoryRecursive $ installDirectory opts + updatePackageIndex -- Due to caching sometimes the package repository state may lag behind the -- documentation index. Consequently, we make sure that the packages we are -- going to build actually appear in the repository before building. See -- #543. - repoIndex <- parseRepositoryIndices verbosity + repoIndex <- parseRepositoryIndices opts verbosity pkgIdsHaveDocs <- getDocumentationStats verbosity opts config (Just pkgs) infoStats verbosity Nothing pkgIdsHaveDocs @@ -570,9 +588,9 @@ processPkg verbosity opts config docInfo = do let installOk = fmap ("install-outcome: InstallOk" `isInfixOf`) buildReport == Just True -- Run Tests if installOk, Run coverage is Tests runs - (testOutcome, hpcLoc) <- case installOk of + (testOutcome, hpcLoc, testfile) <- case installOk && docInfoRunTests docInfo of True -> testPackage verbosity opts docInfo - False -> return (Nothing, Nothing) + False -> return (Nothing, Nothing, Nothing) coverageFile <- mapM (coveragePackage verbosity opts docInfo) hpcLoc -- Modify test-outcome and rewrite report file. @@ -581,7 +599,7 @@ processPkg verbosity opts config docInfo = do case bo_dryRun opts of True -> return () False -> uploadResults verbosity config docInfo - mTgz mRpt logfile coverageFile installOk + mTgz mRpt logfile testfile coverageFile installOk where prepareTempBuildDir :: IO () prepareTempBuildDir = do @@ -591,7 +609,8 @@ processPkg verbosity opts config docInfo = do createDirectoryIfMissing True $ resultsDirectory opts notice verbosity $ "Writing cabal.project for " ++ display (docInfoPackage docInfo) let projectFile = installDirectory opts "cabal.project" - writeFile projectFile $ "packages: " ++ show (docInfoTarGzURI config docInfo) + cabal opts "unpack" [show (docInfoTarGzURI config docInfo)] Nothing + writeFile projectFile $ "packages: */*.cabal" -- ++ show (docInfoTarGzURI config docInfo) setTestOutcome :: String -> [String] -> [String] setTestOutcome _ [] = [] @@ -630,7 +649,7 @@ coveragePackage verbosity opts docInfo loc = do return coverageFile -testPackage :: Verbosity -> BuildOpts -> DocInfo -> IO (Maybe String, Maybe FilePath) +testPackage :: Verbosity -> BuildOpts -> DocInfo -> IO (Maybe String, Maybe FilePath, Maybe FilePath) testPackage verbosity opts docInfo = do let pkgid = docInfoPackage docInfo testLogFile = (installDirectory opts) display pkgid <.> "test" @@ -663,7 +682,7 @@ testPackage verbosity opts docInfo = do [ "Test results for " ++ display pkgid ++ ":" , testResultFile ] - return (testOutcome, hpcLoc) + return (testOutcome, hpcLoc, Just testResultFile) -- | Build documentation and return @(Just tgz)@ for the built tgz file @@ -855,9 +874,9 @@ tarGzDirectory dir = do where (containing_dir, nested_dir) = splitFileName dir uploadResults :: Verbosity -> BuildConfig -> DocInfo -> Maybe FilePath - -> Maybe FilePath -> FilePath -> Maybe FilePath -> Bool -> IO () + -> Maybe FilePath -> FilePath -> Maybe FilePath -> Maybe FilePath -> Bool -> IO () uploadResults verbosity config docInfo - mdocsTarballFile buildReportFile buildLogFile coverageFile installOk = + mdocsTarballFile buildReportFile buildLogFile testLogFile coverageFile installOk = httpSession verbosity "hackage-build" version $ do -- Make sure we authenticate to Hackage setAuthorityGen (provideAuthInfo (bc_srcURI config) @@ -867,7 +886,7 @@ uploadResults verbosity config docInfo Just docsTarballFile -> putDocsTarball config docInfo docsTarballFile - putBuildFiles config docInfo buildReportFile buildLogFile coverageFile installOk + putBuildFiles config docInfo buildReportFile buildLogFile testLogFile coverageFile installOk putDocsTarball :: BuildConfig -> DocInfo -> FilePath -> HttpSession () putDocsTarball config docInfo docsTarballFile = @@ -875,13 +894,14 @@ putDocsTarball config docInfo docsTarballFile = "application/x-tar" (Just "gzip") docsTarballFile putBuildFiles :: BuildConfig -> DocInfo -> Maybe FilePath - -> FilePath -> Maybe FilePath -> Bool -> HttpSession () -putBuildFiles config docInfo reportFile buildLogFile coverageFile installOk = do + -> FilePath -> Maybe FilePath -> Maybe FilePath -> Bool -> HttpSession () +putBuildFiles config docInfo reportFile buildLogFile testLogFile coverageFile installOk = do reportContent <- liftIO $ traverse readFile reportFile logContent <- liftIO $ readFile buildLogFile + testContent <- liftIO $ traverse readFile testLogFile coverageContent <- liftIO $ traverse readFile coverageFile let uri = docInfoReports config docInfo - body = encode $ BR.BuildFiles reportContent (Just logContent) coverageContent (not installOk) + body = encode $ BR.BuildFiles reportContent (Just logContent) testContent coverageContent (not installOk) setAllowRedirects False (_, response) <- request Request { rqURI = uri, diff --git a/hackage-server.cabal b/hackage-server.cabal index af9413f62..ceb7a6049 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -27,7 +27,7 @@ copyright: 2008-2015 Duncan Coutts, license: BSD-3-Clause license-file: LICENSE -tested-with: GHC == { 9.2.2, 9.0.2, 8.10.7, 8.8.4 } +tested-with: GHC == { 9.4.4, 9.2.5, 9.0.2, 8.10.7, 8.8.4 } data-dir: datafiles data-files: @@ -63,6 +63,12 @@ extra-source-files: tests/unpack-checks/LANGUAGE-GHC-9.2/Main.hs tests/unpack-checks/LANGUAGE-GHC-9.2/Setup.hs tests/unpack-checks/LANGUAGE-GHC-9.2/LANGUAGE-GHC.cabal + libstemmer_c/src_c/stem_ISO_8859_1_english.h + libstemmer_c/include/libstemmer.h + libstemmer_c/runtime/api.h + libstemmer_c/runtime/header.h + libstemmer_c/LICENSE + src/Distribution/Server/Util/NLP/LICENSE source-repository head type: git @@ -99,34 +105,37 @@ common defaults -- see `cabal.project.local-ghc-${VERSION}` files build-depends: , array >= 0.5 && < 0.6 - , base >= 4.13 && < 4.17 + , base >= 4.13 && < 4.18 , binary >= 0.8 && < 0.9 , bytestring >= 0.10 && < 0.12 , containers ^>= 0.6.0 , deepseq >= 1.4 && < 1.5 , directory >= 1.3 && < 1.4 , filepath >= 1.4 && < 1.5 - , mtl ^>= 2.2.1 + , mtl >= 2.2.1 && < 2.4 , pretty >= 1.1 && < 1.2 , process >= 1.6 && < 1.7 - , text ^>= 1.2.5.0 + , text ^>= 1.2.5.0 || ^>= 2.0 , time >= 1.9 && < 1.13 - , transformers >= 0.5 && < 0.6 + , transformers >= 0.5 && < 0.7 , unix >= 2.7 && < 2.8 , scientific -- other dependencies shared by most components build-depends: - , aeson ^>= 2.0.3.0 - , Cabal ^>= 3.6.3.0 + , aeson ^>= 2.0.3.0 || ^>= 2.1.0.0 + , Cabal ^>= 3.8.1.0 + , Cabal-syntax ^>= 3.8.1.0 + -- Cabal-syntax needs to be bound to constrain hackage-security + -- see https://github.com/haskell/hackage-server/issues/1130 , fail ^>= 4.9.0 -- we use Control.Monad.Except, introduced in mtl-2.2.1 - , network >= 3 && < 3.2 + , network >= 3 && < 3.2 , network-bsd ^>= 2.8 , network-uri ^>= 2.6 , parsec ^>= 3.1.13 , tar ^>= 0.5 , unordered-containers ^>= 0.2.10 - , vector ^>= 0.12 + , vector ^>= 0.12 || ^>= 0.13.0.0 , zlib ^>= 0.6.2 ghc-options: -Wall -fwarn-tabs -fno-warn-unused-do-bind -fno-warn-deprecated-flags -funbox-strict-fields @@ -228,7 +237,7 @@ library lib-server Distribution.Server.Pages.Package.HaddockParse Distribution.Server.Pages.Recent Distribution.Server.Pages.AdminLog - -- [reverse index disabled] Distribution.Server.Pages.Reverse + Distribution.Server.Pages.Reverse Distribution.Server.Pages.Template Distribution.Server.Pages.Util @@ -247,6 +256,7 @@ library lib-server Distribution.Server.Util.Parse Distribution.Server.Util.ServeTarball Distribution.Server.Util.Validators + Distribution.Server.Util.Validators.Internal -- [unused] Distribution.Server.Util.TarIndex Distribution.Server.Util.GZip Distribution.Server.Util.ContentType @@ -256,6 +266,7 @@ library lib-server Distribution.Server.Util.Merge Distribution.Server.Util.ParseSpecVer Distribution.Server.Util.Markdown + Distribution.Server.Util.Paging Distribution.Server.Features Distribution.Server.Features.Browse @@ -280,6 +291,7 @@ library lib-server Distribution.Server.Features.Upload.State Distribution.Server.Features.Upload.Backup Distribution.Server.Features.Users + Distribution.Server.Features.UserNotify if flag(minimal) @@ -343,8 +355,8 @@ library lib-server Distribution.Server.Features.PreferredVersions Distribution.Server.Features.PreferredVersions.State Distribution.Server.Features.PreferredVersions.Backup - -- [reverse index disabled] Distribution.Server.Features.ReverseDependencies - -- [reverse index disabled] Distribution.Server.Features.ReverseDependencies.State + Distribution.Server.Features.ReverseDependencies + Distribution.Server.Features.ReverseDependencies.State Distribution.Server.Features.Tags Distribution.Server.Features.Tags.Backup Distribution.Server.Features.Tags.State @@ -355,6 +367,7 @@ library lib-server Distribution.Server.Features.StaticFiles Distribution.Server.Features.ServerIntrospect Distribution.Server.Features.Sitemap + Distribution.Server.Util.NLP.Snowball if flag(debug) cpp-options: -DDEBUG @@ -367,16 +380,17 @@ library lib-server -- NB: see also build-depends in `common defaults`! build-depends: , HStringTemplate ^>= 0.8 - , HTTP ^>= 4000.3.16 + , HTTP ^>= 4000.3.16 || ^>= 4000.4.1 , QuickCheck ^>= 2.14 , acid-state ^>= 0.16 , async ^>= 2.2.1 -- requires bumping http-io-streams , attoparsec ^>= 0.14.4 - , attoparsec-iso8601 ^>= 1.0 + , attoparsec-iso8601 ^>= 1.0 || ^>= 1.1.0.0 , base16-bytestring ^>= 1.0 -- requires bumping http-io-streams , base64-bytestring ^>= 1.2.1.0 + , bimap ^>= 0.3 --NOTE: blaze-builder-0.4 is now a compat package that uses bytestring-0.10 builder , blaze-builder ^>= 0.4 , blaze-html ^>= 0.9 @@ -389,28 +403,40 @@ library lib-server , cryptohash-sha256 ^>= 0.11.100 , csv ^>= 0.1 , ed25519 ^>= 0.0.5 - , hackage-security ^>= 0.6 + , exceptions ^>= 0.10 + , hackage-security >= 0.6 && < 0.7 + -- N.B: hackage-security-0.6.2 uses Cabal-syntax-3.8.1.0 + -- see https://github.com/haskell/hackage-server/issues/1130 + -- Thus, we need to include Cabal-syntax as dependency explicitly , hackage-security-HTTP ^>= 0.1.1 - , haddock-library > 1.7 && < 2 - , happstack-server ^>= 7.7.1 - , hashable ^>= 1.3 + , haddock-library ^>= 1.11.0 + -- haddock-library-1.11.0 changed type of markupOrderedList + -- see https://github.com/haskell/hackage-server/issues/1128 + , happstack-server ^>= 7.7.1 || ^>= 7.8.0 + , hashable ^>= 1.3 || ^>= 1.4 + , hs-captcha ^>= 1.0 , hslogger ^>= 1.3.1 , lifted-base ^>= 0.2.1 , mime-mail ^>= 0.5 , random ^>= 1.2 , rss ^>= 3000.2.0.7 , safecopy ^>= 0.10 - , semigroups ^>= 0.19 + , semigroups ^>= 0.20 , split ^>= 0.2 , stm ^>= 2.5.0 + , stringsearch ^>= 0.3.6.6 , tagged ^>= 0.8.5 , xhtml ^>= 3000.2 , xmlgen ^>= 0.6 , xss-sanitize ^>= 0.3.6 if !flag(minimal) - build-depends: snowball ^>= 1.0 - , tokenize ^>= 0.3 + build-depends: tokenize ^>= 0.3 + + c-sources: libstemmer_c/src_c/stem_ISO_8859_1_english.c + libstemmer_c/runtime/api.c + libstemmer_c/runtime/utilities.c + libstemmer_c/libstemmer/libstemmer.c if flag(cabal-parsers) build-depends: cabal-parsers ^>= 0 @@ -525,6 +551,32 @@ test-suite HighLevelTest , io-streams ^>= 1.5.0.1 , http-io-streams ^>= 0.1.6.1 +test-suite ReverseDependenciesTest + import: test-defaults + type: exitcode-stdio-1.0 + main-is: ReverseDependenciesTest.hs + build-tool-depends: hackage-server:hackage-server + build-depends: + , tasty ^>= 1.4 + , tasty-hunit ^>= 0.10 + , HUnit ^>= 1.6 + , hedgehog ^>= 1.1 + , exceptions + , bimap + other-modules: RevDepCommon + +benchmark RevDeps + import: test-defaults + type: exitcode-stdio-1.0 + hs-source-dirs: tests, benchmarks + main-is: RevDeps.hs + build-tool-depends: hackage-server:hackage-server + build-depends: + , random ^>= 1.2 + , gauge + ghc-options: -with-rtsopts=-s + other-modules: RevDepCommon + test-suite PaginationTest import: test-defaults type: exitcode-stdio-1.0 diff --git a/libstemmer_c/LICENSE b/libstemmer_c/LICENSE new file mode 100644 index 000000000..8615bd9a5 --- /dev/null +++ b/libstemmer_c/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2002, Richard Boulton +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/libstemmer_c/include/libstemmer.h b/libstemmer_c/include/libstemmer.h new file mode 100644 index 000000000..5acb564c3 --- /dev/null +++ b/libstemmer_c/include/libstemmer.h @@ -0,0 +1,20 @@ + +/* Make header file work when included from C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned char sb_symbol; + +struct SN_env * english_ISO_8859_1_stemmer_new(); + +void english_ISO_8859_1_stemmer_delete(struct SN_env * sn_env); + +const sb_symbol * english_ISO_8859_1_stemmer_stem(struct SN_env * sn_env, const sb_symbol * word, int size); + +int english_ISO_8859_1_stemmer_length(struct SN_env * sn_env); + +#ifdef __cplusplus +} +#endif + diff --git a/libstemmer_c/libstemmer/libstemmer.c b/libstemmer_c/libstemmer/libstemmer.c new file mode 100644 index 000000000..446f101a6 --- /dev/null +++ b/libstemmer_c/libstemmer/libstemmer.c @@ -0,0 +1,47 @@ + +#include +#include +#include "../include/libstemmer.h" +#include "../runtime/api.h" +#include "../src_c/stem_ISO_8859_1_english.h" + +extern struct SN_env * +english_ISO_8859_1_stemmer_new() +{ + struct SN_env * sn_env = english_ISO_8859_1_create_env(); + if (sn_env == NULL) + { + english_ISO_8859_1_stemmer_delete(sn_env); + return NULL; + } + + return sn_env; +} + +void +english_ISO_8859_1_stemmer_delete(struct SN_env * sn_env) +{ + if (sn_env == 0) return; + english_ISO_8859_1_close_env(sn_env); +} + +const sb_symbol * +english_ISO_8859_1_stemmer_stem(struct SN_env * sn_env, const sb_symbol * word, int size) +{ + int ret; + if (SN_set_current(sn_env, size, (const symbol *)(word))) + { + sn_env->l = 0; + return NULL; + } + ret = english_ISO_8859_1_stem(sn_env); + if (ret < 0) return NULL; + sn_env->p[sn_env->l] = 0; + return (const sb_symbol *)(sn_env->p); +} + +int +english_ISO_8859_1_stemmer_length(struct SN_env * sn_env) +{ + return sn_env->l; +} diff --git a/libstemmer_c/runtime/api.c b/libstemmer_c/runtime/api.c new file mode 100644 index 000000000..40039ef4a --- /dev/null +++ b/libstemmer_c/runtime/api.c @@ -0,0 +1,66 @@ + +#include /* for calloc, free */ +#include "header.h" + +extern struct SN_env * SN_create_env(int S_size, int I_size, int B_size) +{ + struct SN_env * z = (struct SN_env *) calloc(1, sizeof(struct SN_env)); + if (z == NULL) return NULL; + z->p = create_s(); + if (z->p == NULL) goto error; + if (S_size) + { + int i; + z->S = (symbol * *) calloc(S_size, sizeof(symbol *)); + if (z->S == NULL) goto error; + + for (i = 0; i < S_size; i++) + { + z->S[i] = create_s(); + if (z->S[i] == NULL) goto error; + } + } + + if (I_size) + { + z->I = (int *) calloc(I_size, sizeof(int)); + if (z->I == NULL) goto error; + } + + if (B_size) + { + z->B = (unsigned char *) calloc(B_size, sizeof(unsigned char)); + if (z->B == NULL) goto error; + } + + return z; +error: + SN_close_env(z, S_size); + return NULL; +} + +extern void SN_close_env(struct SN_env * z, int S_size) +{ + if (z == NULL) return; + if (S_size) + { + int i; + for (i = 0; i < S_size; i++) + { + lose_s(z->S[i]); + } + free(z->S); + } + free(z->I); + free(z->B); + if (z->p) lose_s(z->p); + free(z); +} + +extern int SN_set_current(struct SN_env * z, int size, const symbol * s) +{ + int err = replace_s(z, 0, z->l, size, s, NULL); + z->c = 0; + return err; +} + diff --git a/libstemmer_c/runtime/api.h b/libstemmer_c/runtime/api.h new file mode 100644 index 000000000..8b997f0c2 --- /dev/null +++ b/libstemmer_c/runtime/api.h @@ -0,0 +1,26 @@ + +typedef unsigned char symbol; + +/* Or replace 'char' above with 'short' for 16 bit characters. + + More precisely, replace 'char' with whatever type guarantees the + character width you need. Note however that sizeof(symbol) should divide + HEAD, defined in header.h as 2*sizeof(int), without remainder, otherwise + there is an alignment problem. In the unlikely event of a problem here, + consult Martin Porter. + +*/ + +struct SN_env { + symbol * p; + int c; int l; int lb; int bra; int ket; + symbol * * S; + int * I; + unsigned char * B; +}; + +extern struct SN_env * SN_create_env(int S_size, int I_size, int B_size); +extern void SN_close_env(struct SN_env * z, int S_size); + +extern int SN_set_current(struct SN_env * z, int size, const symbol * s); + diff --git a/libstemmer_c/runtime/header.h b/libstemmer_c/runtime/header.h new file mode 100644 index 000000000..4d3078f50 --- /dev/null +++ b/libstemmer_c/runtime/header.h @@ -0,0 +1,58 @@ + +#include + +#include "api.h" + +#define MAXINT INT_MAX +#define MININT INT_MIN + +#define HEAD 2*sizeof(int) + +#define SIZE(p) ((int *)(p))[-1] +#define SET_SIZE(p, n) ((int *)(p))[-1] = n +#define CAPACITY(p) ((int *)(p))[-2] + +struct among +{ int s_size; /* number of chars in string */ + const symbol * s; /* search string */ + int substring_i;/* index to longest matching substring */ + int result; /* result of the lookup */ + int (* function)(struct SN_env *); +}; + +extern symbol * create_s(void); +extern void lose_s(symbol * p); + +extern int skip_utf8(const symbol * p, int c, int lb, int l, int n); + +extern int in_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int in_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); + +extern int in_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int in_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); + +extern int eq_s(struct SN_env * z, int s_size, const symbol * s); +extern int eq_s_b(struct SN_env * z, int s_size, const symbol * s); +extern int eq_v(struct SN_env * z, const symbol * p); +extern int eq_v_b(struct SN_env * z, const symbol * p); + +extern int find_among(struct SN_env * z, const struct among * v, int v_size); +extern int find_among_b(struct SN_env * z, const struct among * v, int v_size); + +extern int replace_s(struct SN_env * z, int c_bra, int c_ket, int s_size, const symbol * s, int * adjustment); +extern int slice_from_s(struct SN_env * z, int s_size, const symbol * s); +extern int slice_from_v(struct SN_env * z, const symbol * p); +extern int slice_del(struct SN_env * z); + +extern int insert_s(struct SN_env * z, int bra, int ket, int s_size, const symbol * s); +extern int insert_v(struct SN_env * z, int bra, int ket, const symbol * p); + +extern symbol * slice_to(struct SN_env * z, symbol * p); +extern symbol * assign_to(struct SN_env * z, symbol * p); + +extern void debug(struct SN_env * z, int number, int line_count); + diff --git a/libstemmer_c/runtime/utilities.c b/libstemmer_c/runtime/utilities.c new file mode 100644 index 000000000..1840f0280 --- /dev/null +++ b/libstemmer_c/runtime/utilities.c @@ -0,0 +1,478 @@ + +#include +#include +#include + +#include "header.h" + +#define unless(C) if(!(C)) + +#define CREATE_SIZE 1 + +extern symbol * create_s(void) { + symbol * p; + void * mem = malloc(HEAD + (CREATE_SIZE + 1) * sizeof(symbol)); + if (mem == NULL) return NULL; + p = (symbol *) (HEAD + (char *) mem); + CAPACITY(p) = CREATE_SIZE; + SET_SIZE(p, CREATE_SIZE); + return p; +} + +extern void lose_s(symbol * p) { + if (p == NULL) return; + free((char *) p - HEAD); +} + +/* + new_p = skip_utf8(p, c, lb, l, n); skips n characters forwards from p + c + if n +ve, or n characters backwards from p + c - 1 if n -ve. new_p is the new + position, or 0 on failure. + + -- used to implement hop and next in the utf8 case. +*/ + +extern int skip_utf8(const symbol * p, int c, int lb, int l, int n) { + int b; + if (n >= 0) { + for (; n > 0; n--) { + if (c >= l) return -1; + b = p[c++]; + if (b >= 0xC0) { /* 1100 0000 */ + while (c < l) { + b = p[c]; + if (b >= 0xC0 || b < 0x80) break; + /* break unless b is 10------ */ + c++; + } + } + } + } else { + for (; n < 0; n++) { + if (c <= lb) return -1; + b = p[--c]; + if (b >= 0x80) { /* 1000 0000 */ + while (c > lb) { + b = p[c]; + if (b >= 0xC0) break; /* 1100 0000 */ + c--; + } + } + } + } + return c; +} + +/* Code for character groupings: utf8 cases */ + +static int get_utf8(const symbol * p, int c, int l, int * slot) { + int b0, b1; + if (c >= l) return 0; + b0 = p[c++]; + if (b0 < 0xC0 || c == l) { /* 1100 0000 */ + * slot = b0; return 1; + } + b1 = p[c++]; + if (b0 < 0xE0 || c == l) { /* 1110 0000 */ + * slot = (b0 & 0x1F) << 6 | (b1 & 0x3F); return 2; + } + * slot = (b0 & 0xF) << 12 | (b1 & 0x3F) << 6 | (p[c] & 0x3F); return 3; +} + +static int get_b_utf8(const symbol * p, int c, int lb, int * slot) { + int b0, b1; + if (c <= lb) return 0; + b0 = p[--c]; + if (b0 < 0x80 || c == lb) { /* 1000 0000 */ + * slot = b0; return 1; + } + b1 = p[--c]; + if (b1 >= 0xC0 || c == lb) { /* 1100 0000 */ + * slot = (b1 & 0x1F) << 6 | (b0 & 0x3F); return 2; + } + * slot = (p[c] & 0xF) << 12 | (b1 & 0x3F) << 6 | (b0 & 0x3F); return 3; +} + +extern int in_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + int w = get_utf8(z->p, z->c, z->l, & ch); + unless (w) return -1; + if (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return w; + z->c += w; + } while (repeat); + return 0; +} + +extern int in_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + int w = get_b_utf8(z->p, z->c, z->lb, & ch); + unless (w) return -1; + if (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return w; + z->c -= w; + } while (repeat); + return 0; +} + +extern int out_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + int w = get_utf8(z->p, z->c, z->l, & ch); + unless (w) return -1; + unless (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return w; + z->c += w; + } while (repeat); + return 0; +} + +extern int out_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + int w = get_b_utf8(z->p, z->c, z->lb, & ch); + unless (w) return -1; + unless (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return w; + z->c -= w; + } while (repeat); + return 0; +} + +/* Code for character groupings: non-utf8 cases */ + +extern int in_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + if (z->c >= z->l) return -1; + ch = z->p[z->c]; + if (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return 1; + z->c++; + } while (repeat); + return 0; +} + +extern int in_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + if (z->c <= z->lb) return -1; + ch = z->p[z->c - 1]; + if (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return 1; + z->c--; + } while (repeat); + return 0; +} + +extern int out_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + if (z->c >= z->l) return -1; + ch = z->p[z->c]; + unless (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return 1; + z->c++; + } while (repeat); + return 0; +} + +extern int out_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat) { + do { + int ch; + if (z->c <= z->lb) return -1; + ch = z->p[z->c - 1]; + unless (ch > max || (ch -= min) < 0 || (s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) + return 1; + z->c--; + } while (repeat); + return 0; +} + +extern int eq_s(struct SN_env * z, int s_size, const symbol * s) { + if (z->l - z->c < s_size || memcmp(z->p + z->c, s, s_size * sizeof(symbol)) != 0) return 0; + z->c += s_size; return 1; +} + +extern int eq_s_b(struct SN_env * z, int s_size, const symbol * s) { + if (z->c - z->lb < s_size || memcmp(z->p + z->c - s_size, s, s_size * sizeof(symbol)) != 0) return 0; + z->c -= s_size; return 1; +} + +extern int eq_v(struct SN_env * z, const symbol * p) { + return eq_s(z, SIZE(p), p); +} + +extern int eq_v_b(struct SN_env * z, const symbol * p) { + return eq_s_b(z, SIZE(p), p); +} + +extern int find_among(struct SN_env * z, const struct among * v, int v_size) { + + int i = 0; + int j = v_size; + + int c = z->c; int l = z->l; + symbol * q = z->p + c; + + const struct among * w; + + int common_i = 0; + int common_j = 0; + + int first_key_inspected = 0; + + while(1) { + int k = i + ((j - i) >> 1); + int diff = 0; + int common = common_i < common_j ? common_i : common_j; /* smaller */ + w = v + k; + { + int i2; for (i2 = common; i2 < w->s_size; i2++) { + if (c + common == l) { diff = -1; break; } + diff = q[common] - w->s[i2]; + if (diff != 0) break; + common++; + } + } + if (diff < 0) { j = k; common_j = common; } + else { i = k; common_i = common; } + if (j - i <= 1) { + if (i > 0) break; /* v->s has been inspected */ + if (j == i) break; /* only one item in v */ + + /* - but now we need to go round once more to get + v->s inspected. This looks messy, but is actually + the optimal approach. */ + + if (first_key_inspected) break; + first_key_inspected = 1; + } + } + while(1) { + w = v + i; + if (common_i >= w->s_size) { + z->c = c + w->s_size; + if (w->function == 0) return w->result; + { + int res = w->function(z); + z->c = c + w->s_size; + if (res) return w->result; + } + } + i = w->substring_i; + if (i < 0) return 0; + } +} + +/* find_among_b is for backwards processing. Same comments apply */ + +extern int find_among_b(struct SN_env * z, const struct among * v, int v_size) { + + int i = 0; + int j = v_size; + + int c = z->c; int lb = z->lb; + symbol * q = z->p + c - 1; + + const struct among * w; + + int common_i = 0; + int common_j = 0; + + int first_key_inspected = 0; + + while(1) { + int k = i + ((j - i) >> 1); + int diff = 0; + int common = common_i < common_j ? common_i : common_j; + w = v + k; + { + int i2; for (i2 = w->s_size - 1 - common; i2 >= 0; i2--) { + if (c - common == lb) { diff = -1; break; } + diff = q[- common] - w->s[i2]; + if (diff != 0) break; + common++; + } + } + if (diff < 0) { j = k; common_j = common; } + else { i = k; common_i = common; } + if (j - i <= 1) { + if (i > 0) break; + if (j == i) break; + if (first_key_inspected) break; + first_key_inspected = 1; + } + } + while(1) { + w = v + i; + if (common_i >= w->s_size) { + z->c = c - w->s_size; + if (w->function == 0) return w->result; + { + int res = w->function(z); + z->c = c - w->s_size; + if (res) return w->result; + } + } + i = w->substring_i; + if (i < 0) return 0; + } +} + + +/* Increase the size of the buffer pointed to by p to at least n symbols. + * If insufficient memory, returns NULL and frees the old buffer. + */ +static symbol * increase_size(symbol * p, int n) { + symbol * q; + int new_size = n + 20; + void * mem = realloc((char *) p - HEAD, + HEAD + (new_size + 1) * sizeof(symbol)); + if (mem == NULL) { + lose_s(p); + return NULL; + } + q = (symbol *) (HEAD + (char *)mem); + CAPACITY(q) = new_size; + return q; +} + +/* to replace symbols between c_bra and c_ket in z->p by the + s_size symbols at s. + Returns 0 on success, -1 on error. + Also, frees z->p (and sets it to NULL) on error. +*/ +extern int replace_s(struct SN_env * z, int c_bra, int c_ket, int s_size, const symbol * s, int * adjptr) +{ + int adjustment; + int len; + if (z->p == NULL) { + z->p = create_s(); + if (z->p == NULL) return -1; + } + adjustment = s_size - (c_ket - c_bra); + len = SIZE(z->p); + if (adjustment != 0) { + if (adjustment + len > CAPACITY(z->p)) { + z->p = increase_size(z->p, adjustment + len); + if (z->p == NULL) return -1; + } + memmove(z->p + c_ket + adjustment, + z->p + c_ket, + (len - c_ket) * sizeof(symbol)); + SET_SIZE(z->p, adjustment + len); + z->l += adjustment; + if (z->c >= c_ket) + z->c += adjustment; + else + if (z->c > c_bra) + z->c = c_bra; + } + unless (s_size == 0) memmove(z->p + c_bra, s, s_size * sizeof(symbol)); + if (adjptr != NULL) + *adjptr = adjustment; + return 0; +} + +static int slice_check(struct SN_env * z) { + + if (z->bra < 0 || + z->bra > z->ket || + z->ket > z->l || + z->p == NULL || + z->l > SIZE(z->p)) /* this line could be removed */ + { +#if 0 + fprintf(stderr, "faulty slice operation:\n"); + debug(z, -1, 0); +#endif + return -1; + } + return 0; +} + +extern int slice_from_s(struct SN_env * z, int s_size, const symbol * s) { + if (slice_check(z)) return -1; + return replace_s(z, z->bra, z->ket, s_size, s, NULL); +} + +extern int slice_from_v(struct SN_env * z, const symbol * p) { + return slice_from_s(z, SIZE(p), p); +} + +extern int slice_del(struct SN_env * z) { + return slice_from_s(z, 0, 0); +} + +extern int insert_s(struct SN_env * z, int bra, int ket, int s_size, const symbol * s) { + int adjustment; + if (replace_s(z, bra, ket, s_size, s, &adjustment)) + return -1; + if (bra <= z->bra) z->bra += adjustment; + if (bra <= z->ket) z->ket += adjustment; + return 0; +} + +extern int insert_v(struct SN_env * z, int bra, int ket, const symbol * p) { + int adjustment; + if (replace_s(z, bra, ket, SIZE(p), p, &adjustment)) + return -1; + if (bra <= z->bra) z->bra += adjustment; + if (bra <= z->ket) z->ket += adjustment; + return 0; +} + +extern symbol * slice_to(struct SN_env * z, symbol * p) { + if (slice_check(z)) { + lose_s(p); + return NULL; + } + { + int len = z->ket - z->bra; + if (CAPACITY(p) < len) { + p = increase_size(p, len); + if (p == NULL) + return NULL; + } + memmove(p, z->p + z->bra, len * sizeof(symbol)); + SET_SIZE(p, len); + } + return p; +} + +extern symbol * assign_to(struct SN_env * z, symbol * p) { + int len = z->l; + if (CAPACITY(p) < len) { + p = increase_size(p, len); + if (p == NULL) + return NULL; + } + memmove(p, z->p, len * sizeof(symbol)); + SET_SIZE(p, len); + return p; +} + +#if 0 +extern void debug(struct SN_env * z, int number, int line_count) { + int i; + int limit = SIZE(z->p); + /*if (number >= 0) printf("%3d (line %4d): '", number, line_count);*/ + if (number >= 0) printf("%3d (line %4d): [%d]'", number, line_count,limit); + for (i = 0; i <= limit; i++) { + if (z->lb == i) printf("{"); + if (z->bra == i) printf("["); + if (z->c == i) printf("|"); + if (z->ket == i) printf("]"); + if (z->l == i) printf("}"); + if (i < limit) + { int ch = z->p[i]; + if (ch == 0) ch = '#'; + printf("%c", ch); + } + } + printf("'\n"); +} +#endif diff --git a/libstemmer_c/src_c/stem_ISO_8859_1_english.c b/libstemmer_c/src_c/stem_ISO_8859_1_english.c new file mode 100644 index 000000000..556b6ab89 --- /dev/null +++ b/libstemmer_c/src_c/stem_ISO_8859_1_english.c @@ -0,0 +1,1117 @@ + +/* This file was generated automatically by the Snowball to ANSI C compiler */ + +#include "../runtime/header.h" + +#ifdef __cplusplus +extern "C" { +#endif +extern int english_ISO_8859_1_stem(struct SN_env * z); +#ifdef __cplusplus +} +#endif +static int r_exception2(struct SN_env * z); +static int r_exception1(struct SN_env * z); +static int r_Step_5(struct SN_env * z); +static int r_Step_4(struct SN_env * z); +static int r_Step_3(struct SN_env * z); +static int r_Step_2(struct SN_env * z); +static int r_Step_1c(struct SN_env * z); +static int r_Step_1b(struct SN_env * z); +static int r_Step_1a(struct SN_env * z); +static int r_R2(struct SN_env * z); +static int r_R1(struct SN_env * z); +static int r_shortv(struct SN_env * z); +static int r_mark_regions(struct SN_env * z); +static int r_postlude(struct SN_env * z); +static int r_prelude(struct SN_env * z); +#ifdef __cplusplus +extern "C" { +#endif + + +extern struct SN_env * english_ISO_8859_1_create_env(void); +extern void english_ISO_8859_1_close_env(struct SN_env * z); + + +#ifdef __cplusplus +} +#endif +static const symbol s_0_0[5] = { 'a', 'r', 's', 'e', 'n' }; +static const symbol s_0_1[6] = { 'c', 'o', 'm', 'm', 'u', 'n' }; +static const symbol s_0_2[5] = { 'g', 'e', 'n', 'e', 'r' }; + +static const struct among a_0[3] = +{ +/* 0 */ { 5, s_0_0, -1, -1, 0}, +/* 1 */ { 6, s_0_1, -1, -1, 0}, +/* 2 */ { 5, s_0_2, -1, -1, 0} +}; + +static const symbol s_1_0[1] = { '\'' }; +static const symbol s_1_1[3] = { '\'', 's', '\'' }; +static const symbol s_1_2[2] = { '\'', 's' }; + +static const struct among a_1[3] = +{ +/* 0 */ { 1, s_1_0, -1, 1, 0}, +/* 1 */ { 3, s_1_1, 0, 1, 0}, +/* 2 */ { 2, s_1_2, -1, 1, 0} +}; + +static const symbol s_2_0[3] = { 'i', 'e', 'd' }; +static const symbol s_2_1[1] = { 's' }; +static const symbol s_2_2[3] = { 'i', 'e', 's' }; +static const symbol s_2_3[4] = { 's', 's', 'e', 's' }; +static const symbol s_2_4[2] = { 's', 's' }; +static const symbol s_2_5[2] = { 'u', 's' }; + +static const struct among a_2[6] = +{ +/* 0 */ { 3, s_2_0, -1, 2, 0}, +/* 1 */ { 1, s_2_1, -1, 3, 0}, +/* 2 */ { 3, s_2_2, 1, 2, 0}, +/* 3 */ { 4, s_2_3, 1, 1, 0}, +/* 4 */ { 2, s_2_4, 1, -1, 0}, +/* 5 */ { 2, s_2_5, 1, -1, 0} +}; + +static const symbol s_3_1[2] = { 'b', 'b' }; +static const symbol s_3_2[2] = { 'd', 'd' }; +static const symbol s_3_3[2] = { 'f', 'f' }; +static const symbol s_3_4[2] = { 'g', 'g' }; +static const symbol s_3_5[2] = { 'b', 'l' }; +static const symbol s_3_6[2] = { 'm', 'm' }; +static const symbol s_3_7[2] = { 'n', 'n' }; +static const symbol s_3_8[2] = { 'p', 'p' }; +static const symbol s_3_9[2] = { 'r', 'r' }; +static const symbol s_3_10[2] = { 'a', 't' }; +static const symbol s_3_11[2] = { 't', 't' }; +static const symbol s_3_12[2] = { 'i', 'z' }; + +static const struct among a_3[13] = +{ +/* 0 */ { 0, 0, -1, 3, 0}, +/* 1 */ { 2, s_3_1, 0, 2, 0}, +/* 2 */ { 2, s_3_2, 0, 2, 0}, +/* 3 */ { 2, s_3_3, 0, 2, 0}, +/* 4 */ { 2, s_3_4, 0, 2, 0}, +/* 5 */ { 2, s_3_5, 0, 1, 0}, +/* 6 */ { 2, s_3_6, 0, 2, 0}, +/* 7 */ { 2, s_3_7, 0, 2, 0}, +/* 8 */ { 2, s_3_8, 0, 2, 0}, +/* 9 */ { 2, s_3_9, 0, 2, 0}, +/* 10 */ { 2, s_3_10, 0, 1, 0}, +/* 11 */ { 2, s_3_11, 0, 2, 0}, +/* 12 */ { 2, s_3_12, 0, 1, 0} +}; + +static const symbol s_4_0[2] = { 'e', 'd' }; +static const symbol s_4_1[3] = { 'e', 'e', 'd' }; +static const symbol s_4_2[3] = { 'i', 'n', 'g' }; +static const symbol s_4_3[4] = { 'e', 'd', 'l', 'y' }; +static const symbol s_4_4[5] = { 'e', 'e', 'd', 'l', 'y' }; +static const symbol s_4_5[5] = { 'i', 'n', 'g', 'l', 'y' }; + +static const struct among a_4[6] = +{ +/* 0 */ { 2, s_4_0, -1, 2, 0}, +/* 1 */ { 3, s_4_1, 0, 1, 0}, +/* 2 */ { 3, s_4_2, -1, 2, 0}, +/* 3 */ { 4, s_4_3, -1, 2, 0}, +/* 4 */ { 5, s_4_4, 3, 1, 0}, +/* 5 */ { 5, s_4_5, -1, 2, 0} +}; + +static const symbol s_5_0[4] = { 'a', 'n', 'c', 'i' }; +static const symbol s_5_1[4] = { 'e', 'n', 'c', 'i' }; +static const symbol s_5_2[3] = { 'o', 'g', 'i' }; +static const symbol s_5_3[2] = { 'l', 'i' }; +static const symbol s_5_4[3] = { 'b', 'l', 'i' }; +static const symbol s_5_5[4] = { 'a', 'b', 'l', 'i' }; +static const symbol s_5_6[4] = { 'a', 'l', 'l', 'i' }; +static const symbol s_5_7[5] = { 'f', 'u', 'l', 'l', 'i' }; +static const symbol s_5_8[6] = { 'l', 'e', 's', 's', 'l', 'i' }; +static const symbol s_5_9[5] = { 'o', 'u', 's', 'l', 'i' }; +static const symbol s_5_10[5] = { 'e', 'n', 't', 'l', 'i' }; +static const symbol s_5_11[5] = { 'a', 'l', 'i', 't', 'i' }; +static const symbol s_5_12[6] = { 'b', 'i', 'l', 'i', 't', 'i' }; +static const symbol s_5_13[5] = { 'i', 'v', 'i', 't', 'i' }; +static const symbol s_5_14[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_5_15[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_5_16[5] = { 'a', 'l', 'i', 's', 'm' }; +static const symbol s_5_17[5] = { 'a', 't', 'i', 'o', 'n' }; +static const symbol s_5_18[7] = { 'i', 'z', 'a', 't', 'i', 'o', 'n' }; +static const symbol s_5_19[4] = { 'i', 'z', 'e', 'r' }; +static const symbol s_5_20[4] = { 'a', 't', 'o', 'r' }; +static const symbol s_5_21[7] = { 'i', 'v', 'e', 'n', 'e', 's', 's' }; +static const symbol s_5_22[7] = { 'f', 'u', 'l', 'n', 'e', 's', 's' }; +static const symbol s_5_23[7] = { 'o', 'u', 's', 'n', 'e', 's', 's' }; + +static const struct among a_5[24] = +{ +/* 0 */ { 4, s_5_0, -1, 3, 0}, +/* 1 */ { 4, s_5_1, -1, 2, 0}, +/* 2 */ { 3, s_5_2, -1, 13, 0}, +/* 3 */ { 2, s_5_3, -1, 16, 0}, +/* 4 */ { 3, s_5_4, 3, 12, 0}, +/* 5 */ { 4, s_5_5, 4, 4, 0}, +/* 6 */ { 4, s_5_6, 3, 8, 0}, +/* 7 */ { 5, s_5_7, 3, 14, 0}, +/* 8 */ { 6, s_5_8, 3, 15, 0}, +/* 9 */ { 5, s_5_9, 3, 10, 0}, +/* 10 */ { 5, s_5_10, 3, 5, 0}, +/* 11 */ { 5, s_5_11, -1, 8, 0}, +/* 12 */ { 6, s_5_12, -1, 12, 0}, +/* 13 */ { 5, s_5_13, -1, 11, 0}, +/* 14 */ { 6, s_5_14, -1, 1, 0}, +/* 15 */ { 7, s_5_15, 14, 7, 0}, +/* 16 */ { 5, s_5_16, -1, 8, 0}, +/* 17 */ { 5, s_5_17, -1, 7, 0}, +/* 18 */ { 7, s_5_18, 17, 6, 0}, +/* 19 */ { 4, s_5_19, -1, 6, 0}, +/* 20 */ { 4, s_5_20, -1, 7, 0}, +/* 21 */ { 7, s_5_21, -1, 11, 0}, +/* 22 */ { 7, s_5_22, -1, 9, 0}, +/* 23 */ { 7, s_5_23, -1, 10, 0} +}; + +static const symbol s_6_0[5] = { 'i', 'c', 'a', 't', 'e' }; +static const symbol s_6_1[5] = { 'a', 't', 'i', 'v', 'e' }; +static const symbol s_6_2[5] = { 'a', 'l', 'i', 'z', 'e' }; +static const symbol s_6_3[5] = { 'i', 'c', 'i', 't', 'i' }; +static const symbol s_6_4[4] = { 'i', 'c', 'a', 'l' }; +static const symbol s_6_5[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_6_6[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_6_7[3] = { 'f', 'u', 'l' }; +static const symbol s_6_8[4] = { 'n', 'e', 's', 's' }; + +static const struct among a_6[9] = +{ +/* 0 */ { 5, s_6_0, -1, 4, 0}, +/* 1 */ { 5, s_6_1, -1, 6, 0}, +/* 2 */ { 5, s_6_2, -1, 3, 0}, +/* 3 */ { 5, s_6_3, -1, 4, 0}, +/* 4 */ { 4, s_6_4, -1, 4, 0}, +/* 5 */ { 6, s_6_5, -1, 1, 0}, +/* 6 */ { 7, s_6_6, 5, 2, 0}, +/* 7 */ { 3, s_6_7, -1, 5, 0}, +/* 8 */ { 4, s_6_8, -1, 5, 0} +}; + +static const symbol s_7_0[2] = { 'i', 'c' }; +static const symbol s_7_1[4] = { 'a', 'n', 'c', 'e' }; +static const symbol s_7_2[4] = { 'e', 'n', 'c', 'e' }; +static const symbol s_7_3[4] = { 'a', 'b', 'l', 'e' }; +static const symbol s_7_4[4] = { 'i', 'b', 'l', 'e' }; +static const symbol s_7_5[3] = { 'a', 't', 'e' }; +static const symbol s_7_6[3] = { 'i', 'v', 'e' }; +static const symbol s_7_7[3] = { 'i', 'z', 'e' }; +static const symbol s_7_8[3] = { 'i', 't', 'i' }; +static const symbol s_7_9[2] = { 'a', 'l' }; +static const symbol s_7_10[3] = { 'i', 's', 'm' }; +static const symbol s_7_11[3] = { 'i', 'o', 'n' }; +static const symbol s_7_12[2] = { 'e', 'r' }; +static const symbol s_7_13[3] = { 'o', 'u', 's' }; +static const symbol s_7_14[3] = { 'a', 'n', 't' }; +static const symbol s_7_15[3] = { 'e', 'n', 't' }; +static const symbol s_7_16[4] = { 'm', 'e', 'n', 't' }; +static const symbol s_7_17[5] = { 'e', 'm', 'e', 'n', 't' }; + +static const struct among a_7[18] = +{ +/* 0 */ { 2, s_7_0, -1, 1, 0}, +/* 1 */ { 4, s_7_1, -1, 1, 0}, +/* 2 */ { 4, s_7_2, -1, 1, 0}, +/* 3 */ { 4, s_7_3, -1, 1, 0}, +/* 4 */ { 4, s_7_4, -1, 1, 0}, +/* 5 */ { 3, s_7_5, -1, 1, 0}, +/* 6 */ { 3, s_7_6, -1, 1, 0}, +/* 7 */ { 3, s_7_7, -1, 1, 0}, +/* 8 */ { 3, s_7_8, -1, 1, 0}, +/* 9 */ { 2, s_7_9, -1, 1, 0}, +/* 10 */ { 3, s_7_10, -1, 1, 0}, +/* 11 */ { 3, s_7_11, -1, 2, 0}, +/* 12 */ { 2, s_7_12, -1, 1, 0}, +/* 13 */ { 3, s_7_13, -1, 1, 0}, +/* 14 */ { 3, s_7_14, -1, 1, 0}, +/* 15 */ { 3, s_7_15, -1, 1, 0}, +/* 16 */ { 4, s_7_16, 15, 1, 0}, +/* 17 */ { 5, s_7_17, 16, 1, 0} +}; + +static const symbol s_8_0[1] = { 'e' }; +static const symbol s_8_1[1] = { 'l' }; + +static const struct among a_8[2] = +{ +/* 0 */ { 1, s_8_0, -1, 1, 0}, +/* 1 */ { 1, s_8_1, -1, 2, 0} +}; + +static const symbol s_9_0[7] = { 's', 'u', 'c', 'c', 'e', 'e', 'd' }; +static const symbol s_9_1[7] = { 'p', 'r', 'o', 'c', 'e', 'e', 'd' }; +static const symbol s_9_2[6] = { 'e', 'x', 'c', 'e', 'e', 'd' }; +static const symbol s_9_3[7] = { 'c', 'a', 'n', 'n', 'i', 'n', 'g' }; +static const symbol s_9_4[6] = { 'i', 'n', 'n', 'i', 'n', 'g' }; +static const symbol s_9_5[7] = { 'e', 'a', 'r', 'r', 'i', 'n', 'g' }; +static const symbol s_9_6[7] = { 'h', 'e', 'r', 'r', 'i', 'n', 'g' }; +static const symbol s_9_7[6] = { 'o', 'u', 't', 'i', 'n', 'g' }; + +static const struct among a_9[8] = +{ +/* 0 */ { 7, s_9_0, -1, -1, 0}, +/* 1 */ { 7, s_9_1, -1, -1, 0}, +/* 2 */ { 6, s_9_2, -1, -1, 0}, +/* 3 */ { 7, s_9_3, -1, -1, 0}, +/* 4 */ { 6, s_9_4, -1, -1, 0}, +/* 5 */ { 7, s_9_5, -1, -1, 0}, +/* 6 */ { 7, s_9_6, -1, -1, 0}, +/* 7 */ { 6, s_9_7, -1, -1, 0} +}; + +static const symbol s_10_0[5] = { 'a', 'n', 'd', 'e', 's' }; +static const symbol s_10_1[5] = { 'a', 't', 'l', 'a', 's' }; +static const symbol s_10_2[4] = { 'b', 'i', 'a', 's' }; +static const symbol s_10_3[6] = { 'c', 'o', 's', 'm', 'o', 's' }; +static const symbol s_10_4[5] = { 'd', 'y', 'i', 'n', 'g' }; +static const symbol s_10_5[5] = { 'e', 'a', 'r', 'l', 'y' }; +static const symbol s_10_6[6] = { 'g', 'e', 'n', 't', 'l', 'y' }; +static const symbol s_10_7[4] = { 'h', 'o', 'w', 'e' }; +static const symbol s_10_8[4] = { 'i', 'd', 'l', 'y' }; +static const symbol s_10_9[5] = { 'l', 'y', 'i', 'n', 'g' }; +static const symbol s_10_10[4] = { 'n', 'e', 'w', 's' }; +static const symbol s_10_11[4] = { 'o', 'n', 'l', 'y' }; +static const symbol s_10_12[6] = { 's', 'i', 'n', 'g', 'l', 'y' }; +static const symbol s_10_13[5] = { 's', 'k', 'i', 'e', 's' }; +static const symbol s_10_14[4] = { 's', 'k', 'i', 's' }; +static const symbol s_10_15[3] = { 's', 'k', 'y' }; +static const symbol s_10_16[5] = { 't', 'y', 'i', 'n', 'g' }; +static const symbol s_10_17[4] = { 'u', 'g', 'l', 'y' }; + +static const struct among a_10[18] = +{ +/* 0 */ { 5, s_10_0, -1, -1, 0}, +/* 1 */ { 5, s_10_1, -1, -1, 0}, +/* 2 */ { 4, s_10_2, -1, -1, 0}, +/* 3 */ { 6, s_10_3, -1, -1, 0}, +/* 4 */ { 5, s_10_4, -1, 3, 0}, +/* 5 */ { 5, s_10_5, -1, 9, 0}, +/* 6 */ { 6, s_10_6, -1, 7, 0}, +/* 7 */ { 4, s_10_7, -1, -1, 0}, +/* 8 */ { 4, s_10_8, -1, 6, 0}, +/* 9 */ { 5, s_10_9, -1, 4, 0}, +/* 10 */ { 4, s_10_10, -1, -1, 0}, +/* 11 */ { 4, s_10_11, -1, 10, 0}, +/* 12 */ { 6, s_10_12, -1, 11, 0}, +/* 13 */ { 5, s_10_13, -1, 2, 0}, +/* 14 */ { 4, s_10_14, -1, 1, 0}, +/* 15 */ { 3, s_10_15, -1, -1, 0}, +/* 16 */ { 5, s_10_16, -1, 5, 0}, +/* 17 */ { 4, s_10_17, -1, 8, 0} +}; + +static const unsigned char g_v[] = { 17, 65, 16, 1 }; + +static const unsigned char g_v_WXY[] = { 1, 17, 65, 208, 1 }; + +static const unsigned char g_valid_LI[] = { 55, 141, 2 }; + +static const symbol s_0[] = { '\'' }; +static const symbol s_1[] = { 'y' }; +static const symbol s_2[] = { 'Y' }; +static const symbol s_3[] = { 'y' }; +static const symbol s_4[] = { 'Y' }; +static const symbol s_5[] = { 's', 's' }; +static const symbol s_6[] = { 'i' }; +static const symbol s_7[] = { 'i', 'e' }; +static const symbol s_8[] = { 'e', 'e' }; +static const symbol s_9[] = { 'e' }; +static const symbol s_10[] = { 'e' }; +static const symbol s_11[] = { 'y' }; +static const symbol s_12[] = { 'Y' }; +static const symbol s_13[] = { 'i' }; +static const symbol s_14[] = { 't', 'i', 'o', 'n' }; +static const symbol s_15[] = { 'e', 'n', 'c', 'e' }; +static const symbol s_16[] = { 'a', 'n', 'c', 'e' }; +static const symbol s_17[] = { 'a', 'b', 'l', 'e' }; +static const symbol s_18[] = { 'e', 'n', 't' }; +static const symbol s_19[] = { 'i', 'z', 'e' }; +static const symbol s_20[] = { 'a', 't', 'e' }; +static const symbol s_21[] = { 'a', 'l' }; +static const symbol s_22[] = { 'f', 'u', 'l' }; +static const symbol s_23[] = { 'o', 'u', 's' }; +static const symbol s_24[] = { 'i', 'v', 'e' }; +static const symbol s_25[] = { 'b', 'l', 'e' }; +static const symbol s_26[] = { 'l' }; +static const symbol s_27[] = { 'o', 'g' }; +static const symbol s_28[] = { 'f', 'u', 'l' }; +static const symbol s_29[] = { 'l', 'e', 's', 's' }; +static const symbol s_30[] = { 't', 'i', 'o', 'n' }; +static const symbol s_31[] = { 'a', 't', 'e' }; +static const symbol s_32[] = { 'a', 'l' }; +static const symbol s_33[] = { 'i', 'c' }; +static const symbol s_34[] = { 's' }; +static const symbol s_35[] = { 't' }; +static const symbol s_36[] = { 'l' }; +static const symbol s_37[] = { 's', 'k', 'i' }; +static const symbol s_38[] = { 's', 'k', 'y' }; +static const symbol s_39[] = { 'd', 'i', 'e' }; +static const symbol s_40[] = { 'l', 'i', 'e' }; +static const symbol s_41[] = { 't', 'i', 'e' }; +static const symbol s_42[] = { 'i', 'd', 'l' }; +static const symbol s_43[] = { 'g', 'e', 'n', 't', 'l' }; +static const symbol s_44[] = { 'u', 'g', 'l', 'i' }; +static const symbol s_45[] = { 'e', 'a', 'r', 'l', 'i' }; +static const symbol s_46[] = { 'o', 'n', 'l', 'i' }; +static const symbol s_47[] = { 's', 'i', 'n', 'g', 'l' }; +static const symbol s_48[] = { 'Y' }; +static const symbol s_49[] = { 'y' }; + +static int r_prelude(struct SN_env * z) { + z->B[0] = 0; /* unset Y_found, line 26 */ + { int c1 = z->c; /* do, line 27 */ + z->bra = z->c; /* [, line 27 */ + if (!(eq_s(z, 1, s_0))) goto lab0; + z->ket = z->c; /* ], line 27 */ + { int ret = slice_del(z); /* delete, line 27 */ + if (ret < 0) return ret; + } + lab0: + z->c = c1; + } + { int c2 = z->c; /* do, line 28 */ + z->bra = z->c; /* [, line 28 */ + if (!(eq_s(z, 1, s_1))) goto lab1; + z->ket = z->c; /* ], line 28 */ + { int ret = slice_from_s(z, 1, s_2); /* <-, line 28 */ + if (ret < 0) return ret; + } + z->B[0] = 1; /* set Y_found, line 28 */ + lab1: + z->c = c2; + } + { int c3 = z->c; /* do, line 29 */ + while(1) { /* repeat, line 29 */ + int c4 = z->c; + while(1) { /* goto, line 29 */ + int c5 = z->c; + if (in_grouping(z, g_v, 97, 121, 0)) goto lab4; + z->bra = z->c; /* [, line 29 */ + if (!(eq_s(z, 1, s_3))) goto lab4; + z->ket = z->c; /* ], line 29 */ + z->c = c5; + break; + lab4: + z->c = c5; + if (z->c >= z->l) goto lab3; + z->c++; /* goto, line 29 */ + } + { int ret = slice_from_s(z, 1, s_4); /* <-, line 29 */ + if (ret < 0) return ret; + } + z->B[0] = 1; /* set Y_found, line 29 */ + continue; + lab3: + z->c = c4; + break; + } + z->c = c3; + } + return 1; +} + +static int r_mark_regions(struct SN_env * z) { + z->I[0] = z->l; + z->I[1] = z->l; + { int c1 = z->c; /* do, line 35 */ + { int c2 = z->c; /* or, line 41 */ + if (z->c + 4 >= z->l || z->p[z->c + 4] >> 5 != 3 || !((2375680 >> (z->p[z->c + 4] & 0x1f)) & 1)) goto lab2; + if (!(find_among(z, a_0, 3))) goto lab2; /* among, line 36 */ + goto lab1; + lab2: + z->c = c2; + { /* gopast */ /* grouping v, line 41 */ + int ret = out_grouping(z, g_v, 97, 121, 1); + if (ret < 0) goto lab0; + z->c += ret; + } + { /* gopast */ /* non v, line 41 */ + int ret = in_grouping(z, g_v, 97, 121, 1); + if (ret < 0) goto lab0; + z->c += ret; + } + } + lab1: + z->I[0] = z->c; /* setmark p1, line 42 */ + { /* gopast */ /* grouping v, line 43 */ + int ret = out_grouping(z, g_v, 97, 121, 1); + if (ret < 0) goto lab0; + z->c += ret; + } + { /* gopast */ /* non v, line 43 */ + int ret = in_grouping(z, g_v, 97, 121, 1); + if (ret < 0) goto lab0; + z->c += ret; + } + z->I[1] = z->c; /* setmark p2, line 43 */ + lab0: + z->c = c1; + } + return 1; +} + +static int r_shortv(struct SN_env * z) { + { int m1 = z->l - z->c; (void)m1; /* or, line 51 */ + if (out_grouping_b(z, g_v_WXY, 89, 121, 0)) goto lab1; + if (in_grouping_b(z, g_v, 97, 121, 0)) goto lab1; + if (out_grouping_b(z, g_v, 97, 121, 0)) goto lab1; + goto lab0; + lab1: + z->c = z->l - m1; + if (out_grouping_b(z, g_v, 97, 121, 0)) return 0; + if (in_grouping_b(z, g_v, 97, 121, 0)) return 0; + if (z->c > z->lb) return 0; /* atlimit, line 52 */ + } +lab0: + return 1; +} + +static int r_R1(struct SN_env * z) { + if (!(z->I[0] <= z->c)) return 0; + return 1; +} + +static int r_R2(struct SN_env * z) { + if (!(z->I[1] <= z->c)) return 0; + return 1; +} + +static int r_Step_1a(struct SN_env * z) { + int among_var; + { int m_keep = z->l - z->c;/* (void) m_keep;*/ /* try, line 59 */ + z->ket = z->c; /* [, line 60 */ + if (z->c <= z->lb || (z->p[z->c - 1] != 39 && z->p[z->c - 1] != 115)) { z->c = z->l - m_keep; goto lab0; } + among_var = find_among_b(z, a_1, 3); /* substring, line 60 */ + if (!(among_var)) { z->c = z->l - m_keep; goto lab0; } + z->bra = z->c; /* ], line 60 */ + switch(among_var) { + case 0: { z->c = z->l - m_keep; goto lab0; } + case 1: + { int ret = slice_del(z); /* delete, line 62 */ + if (ret < 0) return ret; + } + break; + } + lab0: + ; + } + z->ket = z->c; /* [, line 65 */ + if (z->c <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 115)) return 0; + among_var = find_among_b(z, a_2, 6); /* substring, line 65 */ + if (!(among_var)) return 0; + z->bra = z->c; /* ], line 65 */ + switch(among_var) { + case 0: return 0; + case 1: + { int ret = slice_from_s(z, 2, s_5); /* <-, line 66 */ + if (ret < 0) return ret; + } + break; + case 2: + { int m1 = z->l - z->c; (void)m1; /* or, line 68 */ + { int ret = z->c - 2; + if (z->lb > ret || ret > z->l) goto lab2; + z->c = ret; /* hop, line 68 */ + } + { int ret = slice_from_s(z, 1, s_6); /* <-, line 68 */ + if (ret < 0) return ret; + } + goto lab1; + lab2: + z->c = z->l - m1; + { int ret = slice_from_s(z, 2, s_7); /* <-, line 68 */ + if (ret < 0) return ret; + } + } + lab1: + break; + case 3: + if (z->c <= z->lb) return 0; + z->c--; /* next, line 69 */ + { /* gopast */ /* grouping v, line 69 */ + int ret = out_grouping_b(z, g_v, 97, 121, 1); + if (ret < 0) return 0; + z->c -= ret; + } + { int ret = slice_del(z); /* delete, line 69 */ + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_1b(struct SN_env * z) { + int among_var; + z->ket = z->c; /* [, line 75 */ + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((33554576 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_4, 6); /* substring, line 75 */ + if (!(among_var)) return 0; + z->bra = z->c; /* ], line 75 */ + switch(among_var) { + case 0: return 0; + case 1: + { int ret = r_R1(z); + if (ret == 0) return 0; /* call R1, line 77 */ + if (ret < 0) return ret; + } + { int ret = slice_from_s(z, 2, s_8); /* <-, line 77 */ + if (ret < 0) return ret; + } + break; + case 2: + { int m_test = z->l - z->c; /* test, line 80 */ + { /* gopast */ /* grouping v, line 80 */ + int ret = out_grouping_b(z, g_v, 97, 121, 1); + if (ret < 0) return 0; + z->c -= ret; + } + z->c = z->l - m_test; + } + { int ret = slice_del(z); /* delete, line 80 */ + if (ret < 0) return ret; + } + { int m_test = z->l - z->c; /* test, line 81 */ + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68514004 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 3; else + among_var = find_among_b(z, a_3, 13); /* substring, line 81 */ + if (!(among_var)) return 0; + z->c = z->l - m_test; + } + switch(among_var) { + case 0: return 0; + case 1: + { int c_keep = z->c; + int ret = insert_s(z, z->c, z->c, 1, s_9); /* <+, line 83 */ + z->c = c_keep; + if (ret < 0) return ret; + } + break; + case 2: + z->ket = z->c; /* [, line 86 */ + if (z->c <= z->lb) return 0; + z->c--; /* next, line 86 */ + z->bra = z->c; /* ], line 86 */ + { int ret = slice_del(z); /* delete, line 86 */ + if (ret < 0) return ret; + } + break; + case 3: + if (z->c != z->I[0]) return 0; /* atmark, line 87 */ + { int m_test = z->l - z->c; /* test, line 87 */ + { int ret = r_shortv(z); + if (ret == 0) return 0; /* call shortv, line 87 */ + if (ret < 0) return ret; + } + z->c = z->l - m_test; + } + { int c_keep = z->c; + int ret = insert_s(z, z->c, z->c, 1, s_10); /* <+, line 87 */ + z->c = c_keep; + if (ret < 0) return ret; + } + break; + } + break; + } + return 1; +} + +static int r_Step_1c(struct SN_env * z) { + z->ket = z->c; /* [, line 94 */ + { int m1 = z->l - z->c; (void)m1; /* or, line 94 */ + if (!(eq_s_b(z, 1, s_11))) goto lab1; + goto lab0; + lab1: + z->c = z->l - m1; + if (!(eq_s_b(z, 1, s_12))) return 0; + } +lab0: + z->bra = z->c; /* ], line 94 */ + if (out_grouping_b(z, g_v, 97, 121, 0)) return 0; + { int m2 = z->l - z->c; (void)m2; /* not, line 95 */ + if (z->c > z->lb) goto lab2; /* atlimit, line 95 */ + return 0; + lab2: + z->c = z->l - m2; + } + { int ret = slice_from_s(z, 1, s_13); /* <-, line 96 */ + if (ret < 0) return ret; + } + return 1; +} + +static int r_Step_2(struct SN_env * z) { + int among_var; + z->ket = z->c; /* [, line 100 */ + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((815616 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_5, 24); /* substring, line 100 */ + if (!(among_var)) return 0; + z->bra = z->c; /* ], line 100 */ + { int ret = r_R1(z); + if (ret == 0) return 0; /* call R1, line 100 */ + if (ret < 0) return ret; + } + switch(among_var) { + case 0: return 0; + case 1: + { int ret = slice_from_s(z, 4, s_14); /* <-, line 101 */ + if (ret < 0) return ret; + } + break; + case 2: + { int ret = slice_from_s(z, 4, s_15); /* <-, line 102 */ + if (ret < 0) return ret; + } + break; + case 3: + { int ret = slice_from_s(z, 4, s_16); /* <-, line 103 */ + if (ret < 0) return ret; + } + break; + case 4: + { int ret = slice_from_s(z, 4, s_17); /* <-, line 104 */ + if (ret < 0) return ret; + } + break; + case 5: + { int ret = slice_from_s(z, 3, s_18); /* <-, line 105 */ + if (ret < 0) return ret; + } + break; + case 6: + { int ret = slice_from_s(z, 3, s_19); /* <-, line 107 */ + if (ret < 0) return ret; + } + break; + case 7: + { int ret = slice_from_s(z, 3, s_20); /* <-, line 109 */ + if (ret < 0) return ret; + } + break; + case 8: + { int ret = slice_from_s(z, 2, s_21); /* <-, line 111 */ + if (ret < 0) return ret; + } + break; + case 9: + { int ret = slice_from_s(z, 3, s_22); /* <-, line 112 */ + if (ret < 0) return ret; + } + break; + case 10: + { int ret = slice_from_s(z, 3, s_23); /* <-, line 114 */ + if (ret < 0) return ret; + } + break; + case 11: + { int ret = slice_from_s(z, 3, s_24); /* <-, line 116 */ + if (ret < 0) return ret; + } + break; + case 12: + { int ret = slice_from_s(z, 3, s_25); /* <-, line 118 */ + if (ret < 0) return ret; + } + break; + case 13: + if (!(eq_s_b(z, 1, s_26))) return 0; + { int ret = slice_from_s(z, 2, s_27); /* <-, line 119 */ + if (ret < 0) return ret; + } + break; + case 14: + { int ret = slice_from_s(z, 3, s_28); /* <-, line 120 */ + if (ret < 0) return ret; + } + break; + case 15: + { int ret = slice_from_s(z, 4, s_29); /* <-, line 121 */ + if (ret < 0) return ret; + } + break; + case 16: + if (in_grouping_b(z, g_valid_LI, 99, 116, 0)) return 0; + { int ret = slice_del(z); /* delete, line 122 */ + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_3(struct SN_env * z) { + int among_var; + z->ket = z->c; /* [, line 127 */ + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((528928 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_6, 9); /* substring, line 127 */ + if (!(among_var)) return 0; + z->bra = z->c; /* ], line 127 */ + { int ret = r_R1(z); + if (ret == 0) return 0; /* call R1, line 127 */ + if (ret < 0) return ret; + } + switch(among_var) { + case 0: return 0; + case 1: + { int ret = slice_from_s(z, 4, s_30); /* <-, line 128 */ + if (ret < 0) return ret; + } + break; + case 2: + { int ret = slice_from_s(z, 3, s_31); /* <-, line 129 */ + if (ret < 0) return ret; + } + break; + case 3: + { int ret = slice_from_s(z, 2, s_32); /* <-, line 130 */ + if (ret < 0) return ret; + } + break; + case 4: + { int ret = slice_from_s(z, 2, s_33); /* <-, line 132 */ + if (ret < 0) return ret; + } + break; + case 5: + { int ret = slice_del(z); /* delete, line 134 */ + if (ret < 0) return ret; + } + break; + case 6: + { int ret = r_R2(z); + if (ret == 0) return 0; /* call R2, line 136 */ + if (ret < 0) return ret; + } + { int ret = slice_del(z); /* delete, line 136 */ + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_4(struct SN_env * z) { + int among_var; + z->ket = z->c; /* [, line 141 */ + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1864232 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_7, 18); /* substring, line 141 */ + if (!(among_var)) return 0; + z->bra = z->c; /* ], line 141 */ + { int ret = r_R2(z); + if (ret == 0) return 0; /* call R2, line 141 */ + if (ret < 0) return ret; + } + switch(among_var) { + case 0: return 0; + case 1: + { int ret = slice_del(z); /* delete, line 144 */ + if (ret < 0) return ret; + } + break; + case 2: + { int m1 = z->l - z->c; (void)m1; /* or, line 145 */ + if (!(eq_s_b(z, 1, s_34))) goto lab1; + goto lab0; + lab1: + z->c = z->l - m1; + if (!(eq_s_b(z, 1, s_35))) return 0; + } + lab0: + { int ret = slice_del(z); /* delete, line 145 */ + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_5(struct SN_env * z) { + int among_var; + z->ket = z->c; /* [, line 150 */ + if (z->c <= z->lb || (z->p[z->c - 1] != 101 && z->p[z->c - 1] != 108)) return 0; + among_var = find_among_b(z, a_8, 2); /* substring, line 150 */ + if (!(among_var)) return 0; + z->bra = z->c; /* ], line 150 */ + switch(among_var) { + case 0: return 0; + case 1: + { int m1 = z->l - z->c; (void)m1; /* or, line 151 */ + { int ret = r_R2(z); + if (ret == 0) goto lab1; /* call R2, line 151 */ + if (ret < 0) return ret; + } + goto lab0; + lab1: + z->c = z->l - m1; + { int ret = r_R1(z); + if (ret == 0) return 0; /* call R1, line 151 */ + if (ret < 0) return ret; + } + { int m2 = z->l - z->c; (void)m2; /* not, line 151 */ + { int ret = r_shortv(z); + if (ret == 0) goto lab2; /* call shortv, line 151 */ + if (ret < 0) return ret; + } + return 0; + lab2: + z->c = z->l - m2; + } + } + lab0: + { int ret = slice_del(z); /* delete, line 151 */ + if (ret < 0) return ret; + } + break; + case 2: + { int ret = r_R2(z); + if (ret == 0) return 0; /* call R2, line 152 */ + if (ret < 0) return ret; + } + if (!(eq_s_b(z, 1, s_36))) return 0; + { int ret = slice_del(z); /* delete, line 152 */ + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_exception2(struct SN_env * z) { + z->ket = z->c; /* [, line 158 */ + if (z->c - 5 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 103)) return 0; + if (!(find_among_b(z, a_9, 8))) return 0; /* substring, line 158 */ + z->bra = z->c; /* ], line 158 */ + if (z->c > z->lb) return 0; /* atlimit, line 158 */ + return 1; +} + +static int r_exception1(struct SN_env * z) { + int among_var; + z->bra = z->c; /* [, line 170 */ + if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 3 || !((42750482 >> (z->p[z->c + 2] & 0x1f)) & 1)) return 0; + among_var = find_among(z, a_10, 18); /* substring, line 170 */ + if (!(among_var)) return 0; + z->ket = z->c; /* ], line 170 */ + if (z->c < z->l) return 0; /* atlimit, line 170 */ + switch(among_var) { + case 0: return 0; + case 1: + { int ret = slice_from_s(z, 3, s_37); /* <-, line 174 */ + if (ret < 0) return ret; + } + break; + case 2: + { int ret = slice_from_s(z, 3, s_38); /* <-, line 175 */ + if (ret < 0) return ret; + } + break; + case 3: + { int ret = slice_from_s(z, 3, s_39); /* <-, line 176 */ + if (ret < 0) return ret; + } + break; + case 4: + { int ret = slice_from_s(z, 3, s_40); /* <-, line 177 */ + if (ret < 0) return ret; + } + break; + case 5: + { int ret = slice_from_s(z, 3, s_41); /* <-, line 178 */ + if (ret < 0) return ret; + } + break; + case 6: + { int ret = slice_from_s(z, 3, s_42); /* <-, line 182 */ + if (ret < 0) return ret; + } + break; + case 7: + { int ret = slice_from_s(z, 5, s_43); /* <-, line 183 */ + if (ret < 0) return ret; + } + break; + case 8: + { int ret = slice_from_s(z, 4, s_44); /* <-, line 184 */ + if (ret < 0) return ret; + } + break; + case 9: + { int ret = slice_from_s(z, 5, s_45); /* <-, line 185 */ + if (ret < 0) return ret; + } + break; + case 10: + { int ret = slice_from_s(z, 4, s_46); /* <-, line 186 */ + if (ret < 0) return ret; + } + break; + case 11: + { int ret = slice_from_s(z, 5, s_47); /* <-, line 187 */ + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_postlude(struct SN_env * z) { + if (!(z->B[0])) return 0; /* Boolean test Y_found, line 203 */ + while(1) { /* repeat, line 203 */ + int c1 = z->c; + while(1) { /* goto, line 203 */ + int c2 = z->c; + z->bra = z->c; /* [, line 203 */ + if (!(eq_s(z, 1, s_48))) goto lab1; + z->ket = z->c; /* ], line 203 */ + z->c = c2; + break; + lab1: + z->c = c2; + if (z->c >= z->l) goto lab0; + z->c++; /* goto, line 203 */ + } + { int ret = slice_from_s(z, 1, s_49); /* <-, line 203 */ + if (ret < 0) return ret; + } + continue; + lab0: + z->c = c1; + break; + } + return 1; +} + +extern int english_ISO_8859_1_stem(struct SN_env * z) { + { int c1 = z->c; /* or, line 207 */ + { int ret = r_exception1(z); + if (ret == 0) goto lab1; /* call exception1, line 207 */ + if (ret < 0) return ret; + } + goto lab0; + lab1: + z->c = c1; + { int c2 = z->c; /* not, line 208 */ + { int ret = z->c + 3; + if (0 > ret || ret > z->l) goto lab3; + z->c = ret; /* hop, line 208 */ + } + goto lab2; + lab3: + z->c = c2; + } + goto lab0; + lab2: + z->c = c1; + { int c3 = z->c; /* do, line 209 */ + { int ret = r_prelude(z); + if (ret == 0) goto lab4; /* call prelude, line 209 */ + if (ret < 0) return ret; + } + lab4: + z->c = c3; + } + { int c4 = z->c; /* do, line 210 */ + { int ret = r_mark_regions(z); + if (ret == 0) goto lab5; /* call mark_regions, line 210 */ + if (ret < 0) return ret; + } + lab5: + z->c = c4; + } + z->lb = z->c; z->c = z->l; /* backwards, line 211 */ + + { int m5 = z->l - z->c; (void)m5; /* do, line 213 */ + { int ret = r_Step_1a(z); + if (ret == 0) goto lab6; /* call Step_1a, line 213 */ + if (ret < 0) return ret; + } + lab6: + z->c = z->l - m5; + } + { int m6 = z->l - z->c; (void)m6; /* or, line 215 */ + { int ret = r_exception2(z); + if (ret == 0) goto lab8; /* call exception2, line 215 */ + if (ret < 0) return ret; + } + goto lab7; + lab8: + z->c = z->l - m6; + { int m7 = z->l - z->c; (void)m7; /* do, line 217 */ + { int ret = r_Step_1b(z); + if (ret == 0) goto lab9; /* call Step_1b, line 217 */ + if (ret < 0) return ret; + } + lab9: + z->c = z->l - m7; + } + { int m8 = z->l - z->c; (void)m8; /* do, line 218 */ + { int ret = r_Step_1c(z); + if (ret == 0) goto lab10; /* call Step_1c, line 218 */ + if (ret < 0) return ret; + } + lab10: + z->c = z->l - m8; + } + { int m9 = z->l - z->c; (void)m9; /* do, line 220 */ + { int ret = r_Step_2(z); + if (ret == 0) goto lab11; /* call Step_2, line 220 */ + if (ret < 0) return ret; + } + lab11: + z->c = z->l - m9; + } + { int m10 = z->l - z->c; (void)m10; /* do, line 221 */ + { int ret = r_Step_3(z); + if (ret == 0) goto lab12; /* call Step_3, line 221 */ + if (ret < 0) return ret; + } + lab12: + z->c = z->l - m10; + } + { int m11 = z->l - z->c; (void)m11; /* do, line 222 */ + { int ret = r_Step_4(z); + if (ret == 0) goto lab13; /* call Step_4, line 222 */ + if (ret < 0) return ret; + } + lab13: + z->c = z->l - m11; + } + { int m12 = z->l - z->c; (void)m12; /* do, line 224 */ + { int ret = r_Step_5(z); + if (ret == 0) goto lab14; /* call Step_5, line 224 */ + if (ret < 0) return ret; + } + lab14: + z->c = z->l - m12; + } + } + lab7: + z->c = z->lb; + { int c13 = z->c; /* do, line 227 */ + { int ret = r_postlude(z); + if (ret == 0) goto lab15; /* call postlude, line 227 */ + if (ret < 0) return ret; + } + lab15: + z->c = c13; + } + } +lab0: + return 1; +} + +extern struct SN_env * english_ISO_8859_1_create_env(void) { return SN_create_env(0, 2, 1); } + +extern void english_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } + diff --git a/libstemmer_c/src_c/stem_ISO_8859_1_english.h b/libstemmer_c/src_c/stem_ISO_8859_1_english.h new file mode 100644 index 000000000..e685dcf7e --- /dev/null +++ b/libstemmer_c/src_c/stem_ISO_8859_1_english.h @@ -0,0 +1,16 @@ + +/* This file was generated automatically by the Snowball to ANSI C compiler */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct SN_env * english_ISO_8859_1_create_env(void); +extern void english_ISO_8859_1_close_env(struct SN_env * z); + +extern int english_ISO_8859_1_stem(struct SN_env * z); + +#ifdef __cplusplus +} +#endif + diff --git a/shell.nix b/shell.nix index 95b11a449..909e7de85 100644 --- a/shell.nix +++ b/shell.nix @@ -7,9 +7,9 @@ let pkgs = import nixpkgs { config = { }; }; -in -pkgs.mkShell { - buildInputs = with pkgs; [ +in +with pkgs; pkgs.mkShell rec { + buildInputs = [ # Haskell development cabal-install ghc @@ -22,5 +22,14 @@ pkgs.mkShell { cryptodev pkg-config brotli + + gd + libpng + libjpeg + fontconfig + freetype + expat ]; + + # LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; } diff --git a/src/Distribution/Server/Features.hs b/src/Distribution/Server/Features.hs index f8a8e362e..410a6117f 100644 --- a/src/Distribution/Server/Features.hs +++ b/src/Distribution/Server/Features.hs @@ -33,7 +33,7 @@ import Distribution.Server.Features.BuildReports (initBuildReportsFeature import Distribution.Server.Features.PackageInfoJSON (initPackageInfoJSONFeature) import Distribution.Server.Features.LegacyRedirects (legacyRedirectsFeature) import Distribution.Server.Features.PreferredVersions (initVersionsFeature) --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies (initReverseFeature) +import Distribution.Server.Features.ReverseDependencies (initReverseFeature) import Distribution.Server.Features.DownloadCount (initDownloadFeature) import Distribution.Server.Features.Tags (initTagsFeature) import Distribution.Server.Features.AnalyticsPixels (initAnalyticsPixelsFeature) @@ -49,6 +49,7 @@ import Distribution.Server.Features.AdminLog (initAdminLogFeature) import Distribution.Server.Features.HoogleData (initHoogleDataFeature) import Distribution.Server.Features.Votes (initVotesFeature) import Distribution.Server.Features.Sitemap (initSitemapFeature) +import Distribution.Server.Features.UserNotify (initUserNotifyFeature) import Distribution.Server.Features.PackageFeed (initPackageFeedFeature) #endif import Distribution.Server.Features.ServerIntrospect (serverIntrospectFeature) @@ -132,8 +133,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do initAnalyticsPixelsFeature env mkVersionsFeature <- logStartup "versions" $ initVersionsFeature env - -- mkReverseFeature <- logStartup "reverse deps" $ - -- initReverseFeature env + mkReverseFeature <- logStartup "reverse deps" $ + initReverseFeature env mkListFeature <- logStartup "list" $ initListFeature env mkSearchFeature <- logStartup "search" $ @@ -154,6 +155,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do initAdminLogFeature env mkSitemapFeature <- logStartup "sitemap" $ initSitemapFeature env + mkUserNotifyFeature <- logStartup "user notify" $ + initUserNotifyFeature env mkPackageFeedFeature <- logStartup "package feed" $ initPackageFeedFeature env mkBrowseFeature <- logStartup "browse" $ @@ -193,7 +196,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tarIndexCacheFeature usersFeature - packagesFeature <- mkRecentPackagesFeature + recentPackagesFeature <- mkRecentPackagesFeature usersFeature coreFeature @@ -230,6 +233,17 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do uploadFeature (candidatesCoreResource candidatesFeature) + tagsFeature <- mkTagsFeature + coreFeature + uploadFeature + usersFeature + + versionsFeature <- mkVersionsFeature + coreFeature + uploadFeature + tagsFeature + usersFeature + documentationCoreFeature <- mkDocumentationCoreFeature (coreResource coreFeature) (map packageId . allPackages <$> queryGetPackageIndex coreFeature) @@ -237,6 +251,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tarIndexCacheFeature reportsCoreFeature usersFeature + versionsFeature documentationCandidatesFeature <- mkDocumentationCandidatesFeature (candidatesCoreResource candidatesFeature) @@ -245,6 +260,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tarIndexCacheFeature reportsCandidatesFeature usersFeature + versionsFeature downloadFeature <- mkDownloadFeature coreFeature @@ -254,31 +270,18 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do coreFeature usersFeature - tagsFeature <- mkTagsFeature - coreFeature - uploadFeature - usersFeature - analyticsPixelsFeature <- mkAnalyticsPixelsFeature coreFeature usersFeature uploadFeature - versionsFeature <- mkVersionsFeature - coreFeature - uploadFeature - tagsFeature - usersFeature - - {- [reverse index disabled] reverseFeature <- mkReverseFeature coreFeature versionsFeature - -} listFeature <- mkListFeature coreFeature - -- [reverse index disabled] reverseFeature + reverseFeature downloadFeature votesFeature tagsFeature @@ -299,7 +302,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do uploadFeature candidatesFeature versionsFeature - -- [reverse index disabled] reverseFeature + reverseFeature tagsFeature analyticsPixelsFeature downloadFeature @@ -313,6 +316,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tarIndexCacheFeature reportsCoreFeature userDetailsFeature + recentPackagesFeature + editCabalFeature <- mkEditCabalFilesFeature usersFeature @@ -337,6 +342,16 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do coreFeature documentationCoreFeature tagsFeature + tarIndexCacheFeature + + userNotifyFeature <- mkUserNotifyFeature + usersFeature + coreFeature + uploadFeature + adminLogFeature + userDetailsFeature + reportsCoreFeature + tagsFeature packageFeedFeature <- mkPackageFeedFeature coreFeature @@ -370,7 +385,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do #ifndef MINIMAL , getFeatureInterface tarIndexCacheFeature , getFeatureInterface packageContentsFeature - , getFeatureInterface packagesFeature + , getFeatureInterface recentPackagesFeature , getFeatureInterface userDetailsFeature , getFeatureInterface userSignupFeature , getFeatureInterface legacyPasswdsFeature @@ -384,7 +399,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do , getFeatureInterface tagsFeature , getFeatureInterface analyticsPixelsFeature , getFeatureInterface versionsFeature - -- [reverse index disabled] , getFeatureInterface reverseFeature + , getFeatureInterface reverseFeature , getFeatureInterface searchFeature , getFeatureInterface listFeature , getFeatureInterface platformFeature @@ -396,6 +411,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do , getFeatureInterface votesFeature , getFeatureInterface adminLogFeature , getFeatureInterface siteMapFeature + , getFeatureInterface userNotifyFeature , getFeatureInterface packageFeedFeature , getFeatureInterface packageInfoJSONFeature #endif diff --git a/src/Distribution/Server/Features/AdminLog.hs b/src/Distribution/Server/Features/AdminLog.hs index e32ac14eb..35f01fcf0 100755 --- a/src/Distribution/Server/Features/AdminLog.hs +++ b/src/Distribution/Server/Features/AdminLog.hs @@ -1,6 +1,6 @@ {-# LANGUAGE DeriveDataTypeable, TypeFamilies, TemplateHaskell, BangPatterns, GeneralizedNewtypeDeriving, NamedFieldPuns, RecordWildCards, - PatternGuards #-} + PatternGuards, RankNTypes #-} module Distribution.Server.Features.AdminLog where @@ -77,6 +77,7 @@ makeAcidic ''AdminLog ['getAdminLog data AdminLogFeature = AdminLogFeature { adminLogFeatureInterface :: HackageFeature + , queryGetAdminLog :: forall m. MonadIO m => m AdminLog } instance IsHackageFeature AdminLogFeature where @@ -117,6 +118,9 @@ adminLogFeature UserFeature{..} adminLogState resourceGet = [("html", serveAdminLogGet)] } + queryGetAdminLog :: MonadIO m => m AdminLog + queryGetAdminLog = queryState adminLogState GetAdminLog + serveAdminLogGet _ = do aLog <- queryState adminLogState GetAdminLog users <- queryGetUserDb diff --git a/src/Distribution/Server/Features/Browse.hs b/src/Distribution/Server/Features/Browse.hs index ada4b622c..0ff354f3b 100644 --- a/src/Distribution/Server/Features/Browse.hs +++ b/src/Distribution/Server/Features/Browse.hs @@ -1,7 +1,8 @@ {-# LANGUAGE BlockArguments, NamedFieldPuns #-} module Distribution.Server.Features.Browse (initBrowseFeature, PaginationConfig(..), StartIndex(..), NumElems(..), paginate) where -import Control.Monad.Except (ExceptT, liftIO, throwError) +import Control.Monad.Except (ExceptT, throwError) +import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Class (lift) import qualified Data.Map as Map import Data.Maybe (isJust) @@ -138,7 +139,7 @@ packageIndexInfoToValue :: CoreResource -> TagsResource -> UserResource -> Packa packageIndexInfoToValue coreResource tagsResource userResource PackageItem{itemName, itemDownloads, itemVotes, - itemDesc, itemTags, itemLastUpload, itemMaintainer} = + itemDesc, itemTags, itemLastUpload, itemLastVersion, itemMaintainer} = object [ Key.fromString "name" .= renderPackage itemName , Key.fromString "downloads" .= itemDownloads @@ -146,6 +147,7 @@ packageIndexInfoToValue , Key.fromString "description" .= itemDesc , Key.fromString "tags" .= map renderTag (S.toAscList itemTags) , Key.fromString "lastUpload" .= iso8601Show itemLastUpload + , Key.fromString "lastVersion" .= itemLastVersion , Key.fromString "maintainers" .= map renderUser itemMaintainer ] where diff --git a/src/Distribution/Server/Features/Browse/ApplyFilter.hs b/src/Distribution/Server/Features/Browse/ApplyFilter.hs index f96a3367c..c86082fc8 100644 --- a/src/Distribution/Server/Features/Browse/ApplyFilter.hs +++ b/src/Distribution/Server/Features/Browse/ApplyFilter.hs @@ -63,6 +63,7 @@ sort isSearch sortColumn sortDirection = Description -> comparing itemDesc Tags -> comparing (S.toAscList . itemTags) LastUpload -> comparing itemLastUpload + LastVersion -> comparing itemLastVersion Maintainers -> comparing itemMaintainer in sortBy (maybeReverse comparer) where diff --git a/src/Distribution/Server/Features/Browse/Options.hs b/src/Distribution/Server/Features/Browse/Options.hs index 269be66ef..dd93401ef 100644 --- a/src/Distribution/Server/Features/Browse/Options.hs +++ b/src/Distribution/Server/Features/Browse/Options.hs @@ -9,7 +9,7 @@ import Distribution.Server.Features.Browse.Parsers (Filter, conditions, condsToF data IsSearch = IsSearch | IsNotSearch -data NormalColumn = Name | Downloads | Rating | Description | Tags | LastUpload | Maintainers +data NormalColumn = Name | Downloads | Rating | Description | Tags | LastUpload | LastVersion | Maintainers deriving (Show, Eq) data Column = DefaultColumn | NormalColumn NormalColumn @@ -36,6 +36,7 @@ instance FromJSON Column where "description" -> pure $ NormalColumn Description "tags" -> pure $ NormalColumn Tags "lastUpload" -> pure $ NormalColumn LastUpload + "lastVersion" -> pure $ NormalColumn LastVersion "maintainers" -> pure $ NormalColumn Maintainers t -> fail $ "Column invalid: " ++ T.unpack t @@ -48,6 +49,7 @@ columnToTemplateName = \case NormalColumn Description -> "description" NormalColumn Tags -> "tags" NormalColumn LastUpload -> "lastUpload" + NormalColumn LastVersion -> "lastVersion" NormalColumn Maintainers -> "maintainers" instance FromJSON Direction where diff --git a/src/Distribution/Server/Features/BuildReports.hs b/src/Distribution/Server/Features/BuildReports.hs index 73985286c..300872e11 100644 --- a/src/Distribution/Server/Features/BuildReports.hs +++ b/src/Distribution/Server/Features/BuildReports.hs @@ -6,7 +6,7 @@ module Distribution.Server.Features.BuildReports ( initBuildReportsFeature ) where -import Distribution.Server.Framework hiding (BuildLog, BuildCovg) +import Distribution.Server.Framework hiding (BuildLog, TestLog, BuildCovg) import Distribution.Server.Features.Users import Distribution.Server.Features.Upload @@ -16,7 +16,7 @@ import Distribution.Server.Features.BuildReports.Backup import Distribution.Server.Features.BuildReports.State import qualified Distribution.Server.Features.BuildReports.BuildReport as BuildReport import Distribution.Server.Features.BuildReports.BuildReport (BuildReport(..)) -import Distribution.Server.Features.BuildReports.BuildReports (BuildReports, BuildReportId(..), BuildCovg(..), BuildLog(..)) +import Distribution.Server.Features.BuildReports.BuildReports (BuildReports, BuildReportId(..), BuildCovg(..), BuildLog(..), TestLog(..)) import qualified Distribution.Server.Framework.ResponseContentTypes as Resource import Distribution.Server.Packages.Types @@ -32,6 +32,7 @@ import Data.ByteString.Lazy (toStrict) import Data.String (fromString) import Data.Maybe import Distribution.Compiler ( CompilerId(..) ) +import Data.Aeson (toJSON) -- TODO: @@ -41,12 +42,14 @@ data ReportsFeature = ReportsFeature { reportsFeatureInterface :: HackageFeature, packageReports :: DynamicPath -> ([(BuildReportId, BuildReport)] -> ServerPartE Response) -> ServerPartE Response, - packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg), + packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg), queryPackageReports :: forall m. MonadIO m => PackageId -> m [(BuildReportId, BuildReport)], queryBuildLog :: forall m. MonadIO m => BuildLog -> m Resource.BuildLog, + queryTestLog :: forall m. MonadIO m => TestLog -> m Resource.TestLog, pkgReportDetails :: forall m. MonadIO m => (PackageIdentifier, Bool) -> m BuildReport.PkgDetails, queryLastReportStats:: forall m. MonadIO m => PackageIdentifier -> m (Maybe (BuildReportId, BuildReport, Maybe BuildCovg)), + queryRunTests :: forall m. MonadIO m => PackageId -> m Bool, reportsResource :: ReportsResource } @@ -58,7 +61,9 @@ data ReportsResource = ReportsResource { reportsList :: Resource, reportsPage :: Resource, reportsLog :: Resource, + reportsTest :: Resource, reportsReset:: Resource, + reportsTestsEnabled :: Resource, reportsListUri :: String -> PackageId -> String, reportsPageUri :: String -> PackageId -> BuildReportId -> String, reportsLogUri :: PackageId -> BuildReportId -> String @@ -118,7 +123,9 @@ buildReportsFeature name reportsList , reportsPage , reportsLog + , reportsTest , reportsReset + , reportsTestsEnabled ] , featureState = [abstractAcidStateComponent reportsState] } @@ -140,6 +147,13 @@ buildReportsFeature name ] , resourceGet = [ ("", resetBuildFails) ] } + , reportsTestsEnabled = (extendResourcePath "/reports/testsEnabled/" corePackagePage) { + resourceDesc = [ (GET, "Get reports test settings") + , (POST, "Set reports test settings") + ] + , resourceGet = [ ("json", getReportsTestsEnabled) ] + , resourcePost = [ ("", postReportsTestsEnabled) ] + } , reportsPage = (extendResourcePath "/reports/:id.:format" corePackagePage) { resourceDesc = [ (GET, "Get a specific build report") , (DELETE, "Delete a specific build report") @@ -156,6 +170,15 @@ buildReportsFeature name , resourceDelete = [ ("", deleteBuildLog )] , resourcePut = [ ("", putBuildLog) ] } + , reportsTest = (extendResourcePath "/reports/:id/test" corePackagePage) { + resourceDesc = [ (GET, "Get the test log associated with a build report") + , (DELETE, "Delete a test log") + , (PUT, "Upload a test log for a build report") + ] + , resourceGet = [ ("txt", serveTestLog) ] + , resourceDelete = [ ("", deleteTestLog )] + , resourcePut = [ ("", putTestLog) ] + } , reportsListUri = \format pkgid -> renderResource (reportsList reportsResource) [display pkgid, format] , reportsPageUri = \format pkgid repid -> renderResource (reportsPage reportsResource) [display pkgid, display repid, format] , reportsLogUri = \pkgid repid -> renderResource (reportsLog reportsResource) [display pkgid, display repid] @@ -176,7 +199,7 @@ buildReportsFeature name guardValidPackageId pkgid queryPackageReports pkgid >>= continue - packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg) + packageReport :: DynamicPath -> ServerPartE (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg) packageReport dpath = do pkgid <- packageInPath dpath guardValidPackageId pkgid @@ -184,56 +207,74 @@ buildReportsFeature name mreport <- queryState reportsState $ LookupReportCovg pkgid reportId case mreport of Nothing -> errNotFound "Report not found" [MText "Build report does not exist"] - Just (report, mlog, covg) -> return (reportId, report, mlog, covg) + Just (report, mlog, mtest, covg) -> return (reportId, report, mlog, mtest, covg) queryPackageReports :: MonadIO m => PackageId -> m [(BuildReportId, BuildReport)] queryPackageReports pkgid = do reports <- queryState reportsState $ LookupPackageReports pkgid - return $ map (second fst) reports + return $ map (second (\(a, _, _) -> a)) reports queryBuildLog :: MonadIO m => BuildLog -> m Resource.BuildLog queryBuildLog (BuildLog blobId) = do file <- liftIO $ BlobStorage.fetch store blobId return $ Resource.BuildLog file + queryTestLog :: MonadIO m => TestLog -> m Resource.TestLog + queryTestLog (TestLog blobId) = do + file <- liftIO $ BlobStorage.fetch store blobId + return $ Resource.TestLog file pkgReportDetails :: MonadIO m => (PackageIdentifier, Bool) -> m BuildReport.PkgDetails--(PackageIdentifier, Bool, Maybe (BuildStatus, Maybe UTCTime, Maybe Version)) pkgReportDetails (pkgid, docs) = do failCnt <- queryState reportsState $ LookupFailCount pkgid latestRpt <- queryState reportsState $ LookupLatestReport pkgid + runTests <- fmap Just . queryState reportsState $ LookupRunTests pkgid (time, ghcId) <- case latestRpt of Nothing -> return (Nothing,Nothing) - Just (_, brp, _, _) -> do + Just (_, brp, _, _, _) -> do let (CompilerId _ vrsn) = compiler brp return (time brp, Just vrsn) - return (BuildReport.PkgDetails pkgid docs failCnt time ghcId) + return (BuildReport.PkgDetails pkgid docs failCnt time ghcId runTests) queryLastReportStats :: MonadIO m => PackageIdentifier -> m (Maybe (BuildReportId, BuildReport, Maybe BuildCovg)) queryLastReportStats pkgid = do lookupRes <- queryState reportsState $ LookupLatestReport pkgid case lookupRes of Nothing -> return Nothing - Just (rptId, rpt, _, covg) -> return (Just (rptId, rpt, covg)) + Just (rptId, rpt, _, _, covg) -> return (Just (rptId, rpt, covg)) + queryRunTests :: MonadIO m => PackageId -> m Bool + queryRunTests pkgid = queryState reportsState $ LookupRunTests pkgid --------------------------------------------------------------------------- textPackageReports dpath = packageReports dpath $ return . toResponse . show textPackageReport dpath = do - (_, report, _, _) <- packageReport dpath + (_, report, _, _, _) <- packageReport dpath return . toResponse $ BuildReport.show report -- result: not-found error or text file serveBuildLog :: DynamicPath -> ServerPartE Response serveBuildLog dpath = do - (repid, _, mlog, _) <- packageReport dpath + (repid, _, mlog, _, _) <- packageReport dpath case mlog of Nothing -> errNotFound "Log not found" [MText $ "Build log for report " ++ display repid ++ " not found"] Just logId -> do cacheControlWithoutETag [Public, maxAgeDays 30] toResponse <$> queryBuildLog logId + -- result: not-found error or text file + serveTestLog :: DynamicPath -> ServerPartE Response + serveTestLog dpath = do + (repid, _, _, mtest, _) <- packageReport dpath + case mtest of + Nothing -> errNotFound "Test log not found" [MText $ "Test log for report " ++ display repid ++ " not found"] + Just logId -> do + cacheControlWithoutETag [Public, maxAgeDays 30] + toResponse <$> queryTestLog logId + + -- result: auth error, not-found error, parse error, or redirect submitBuildReport :: DynamicPath -> ServerPartE Response submitBuildReport dpath = do @@ -286,6 +327,18 @@ buildReportsFeature name void $ updateState reportsState $ SetBuildLog pkgid reportId (Just $ BuildLog buildLog) noContent (toResponse ()) + putTestLog :: DynamicPath -> ServerPartE Response + putTestLog dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + reportId <- reportIdInPath dpath + -- logged in users + guardAuthorised_ [AnyKnownUser] + blogbody <- expectTextPlain + testLog <- liftIO $ BlobStorage.add store blogbody + void $ updateState reportsState $ SetTestLog pkgid reportId (Just $ TestLog testLog) + noContent (toResponse ()) + {- Example using curl: (TODO: why is this PUT, while logs are POST?) @@ -305,6 +358,15 @@ buildReportsFeature name void $ updateState reportsState $ SetBuildLog pkgid reportId Nothing noContent (toResponse ()) + deleteTestLog :: DynamicPath -> ServerPartE Response + deleteTestLog dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + reportId <- reportIdInPath dpath + guardAuthorised_ [InGroup trusteesGroup] + void $ updateState reportsState $ SetTestLog pkgid reportId Nothing + noContent (toResponse ()) + guardAuthorisedAsMaintainerOrTrustee pkgname = guardAuthorised_ [InGroup (maintainersGroup pkgname), InGroup trusteesGroup] @@ -318,6 +380,25 @@ buildReportsFeature name then seeOther (reportsListUri reportsResource "" pkgid) $ toResponse () else errNotFound "Report not found" [MText "Build report does not exist"] + getReportsTestsEnabled :: DynamicPath -> ServerPartE Response + getReportsTestsEnabled dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + guardAuthorisedAsMaintainerOrTrustee (packageName pkgid) + runTest <- queryRunTests pkgid + pure $ toResponse $ toJSON runTest + + postReportsTestsEnabled :: DynamicPath -> ServerPartE Response + postReportsTestsEnabled dpath = do + pkgid <- packageInPath dpath + runTests <- body $ looks "runTests" + guardValidPackageId pkgid + guardAuthorisedAsMaintainerOrTrustee (packageName pkgid) + success <- updateState reportsState $ SetRunTests pkgid ("on" `elem` runTests) + if success + then seeOther (reportsListUri reportsResource "" pkgid) $ toResponse () + else errNotFound "Package not found" [MText "Package does not exist"] + putAllReports :: DynamicPath -> ServerPartE Response putAllReports dpath = do @@ -327,6 +408,7 @@ buildReportsFeature name buildFiles <- expectAesonContent::ServerPartE BuildReport.BuildFiles let reportBody = BuildReport.reportContent buildFiles logBody = BuildReport.logContent buildFiles + testBody = BuildReport.testContent buildFiles covgBody = BuildReport.coverageContent buildFiles failStatus = BuildReport.buildFail buildFiles @@ -341,8 +423,9 @@ buildReportsFeature name guardAuthorisedAsMaintainerOrTrustee (packageName pkgid) report' <- liftIO $ BuildReport.affixTimestamp report logBlob <- liftIO $ traverse (\x -> BlobStorage.add store $ fromString x) logBody + testBlob <- liftIO $ traverse (\x -> BlobStorage.add store $ fromString x) testBody reportId <- updateState reportsState $ - AddRptLogCovg pkgid (report', (fmap BuildLog logBlob), (fmap BuildReport.parseCovg covgBody)) + AddRptLogTestCovg pkgid (report', (fmap BuildLog logBlob), (fmap TestLog testBlob), (fmap BuildReport.parseCovg covgBody)) -- redirect to new reports page seeOther (reportsPageUri reportsResource "" pkgid reportId) $ toResponse () diff --git a/src/Distribution/Server/Features/BuildReports/Backup.hs b/src/Distribution/Server/Features/BuildReports/Backup.hs index 5ba7f8081..09e9fe4c5 100644 --- a/src/Distribution/Server/Features/BuildReports/Backup.hs +++ b/src/Distribution/Server/Features/BuildReports/Backup.hs @@ -8,7 +8,7 @@ module Distribution.Server.Features.BuildReports.Backup ( import Distribution.Server.Features.BuildReports.BuildReport (BuildReport) import qualified Distribution.Server.Features.BuildReports.BuildReport as Report -import Distribution.Server.Features.BuildReports.BuildReports (BuildReports(..), BuildCovg(..), PkgBuildReports(..), BuildReportId(..), BuildLog(..)) +import Distribution.Server.Features.BuildReports.BuildReports (BuildReports(..), BuildCovg(..), PkgBuildReports(..), BuildReportId(..), BuildLog(..), TestLog(..)) import qualified Distribution.Server.Features.BuildReports.BuildReports as Reports import qualified Distribution.Server.Framework.BlobStorage as BlobStorage @@ -94,8 +94,8 @@ packageReportsToExport :: PackageId -> PkgBuildReports -> [BackupEntry] packageReportsToExport pkgId pkgReports = concatMap (uncurry $ reportToExport prefix) (Map.toList $ Reports.reports pkgReports) where prefix = ["package", display pkgId] -reportToExport :: [FilePath] -> BuildReportId -> (BuildReport, Maybe BuildLog, Maybe BuildCovg ) -> [BackupEntry] -reportToExport prefix reportId (report, mlog, _) = BackupByteString (getPath ".txt") (packUTF8 $ Report.show report) : +reportToExport :: [FilePath] -> BuildReportId -> (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) -> [BackupEntry] +reportToExport prefix reportId (report, mlog, _, _) = BackupByteString (getPath ".txt") (packUTF8 $ Report.show report) : case mlog of Nothing -> []; Just (BuildLog blobId) -> [blobToBackup (getPath ".log") blobId] where getPath ext = prefix ++ [display reportId ++ ext] diff --git a/src/Distribution/Server/Features/BuildReports/BuildReport.hs b/src/Distribution/Server/Features/BuildReports/BuildReport.hs index 1d85cce5f..ef1231cca 100644 --- a/src/Distribution/Server/Features/BuildReports/BuildReport.hs +++ b/src/Distribution/Server/Features/BuildReports/BuildReport.hs @@ -1,9 +1,11 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} @@ -57,6 +59,10 @@ import Distribution.CabalSpecVersion ( CabalSpecVersion(CabalSpecV2_4) ) import Distribution.Pretty ( Pretty(..), pretty, prettyShow ) +#if MIN_VERSION_Cabal(3,7,0) +import Distribution.Fields.Pretty + ( pattern NoComment ) +#endif import qualified Text.PrettyPrint as Disp import Distribution.Parsec @@ -311,7 +317,13 @@ intPair = do -- Pretty-printing show :: BuildReport -> String -show = showFields (const []) . prettyFieldGrammar CabalSpecV2_4 fieldDescrs +show = showFields noComment . prettyFieldGrammar CabalSpecV2_4 fieldDescrs + where +#if MIN_VERSION_Cabal(3,7,0) + noComment _ = NoComment +#else + noComment _ = [] +#endif -- ----------------------------------------------------------------------------- -- Description of the fields, for parsing/printing @@ -599,6 +611,7 @@ instance Migrate InstallOutcome where data BuildFiles = BuildFiles { reportContent :: Maybe String, logContent :: Maybe String, + testContent :: Maybe String, coverageContent :: Maybe String, buildFail :: Bool } deriving Show @@ -608,6 +621,7 @@ instance Data.Aeson.FromJSON BuildFiles where BuildFiles <$> o .:? "report" <*> o .:? "log" + <*> o .:? "test" <*> o .:? "coverage" <*> o .: "buildFail" @@ -615,6 +629,7 @@ instance Data.Aeson.ToJSON BuildFiles where toJSON p = object [ "report" .= reportContent p, "log" .= logContent p, + "test" .= testContent p, "coverage" .= coverageContent p, "buildFail" .= buildFail p ] @@ -623,7 +638,8 @@ data PkgDetails = PkgDetails { docs :: Bool, failCnt :: Maybe BuildStatus, buildTime :: Maybe UTCTime, - ghcId :: Maybe Version + ghcId :: Maybe Version, + runTests :: Maybe Bool } deriving(Show) instance Data.Aeson.ToJSON PkgDetails where @@ -632,7 +648,8 @@ instance Data.Aeson.ToJSON PkgDetails where "docs" .= docs p, "failCnt" .= failCnt p, "buildTime" .= buildTime p, - "ghcId" .= k (ghcId p) ] + "ghcId" .= k (ghcId p), + "runTests" .= runTests p ] where k (Just a) = Just $ DT.display a k Nothing = Nothing @@ -645,6 +662,7 @@ instance Data.Aeson.FromJSON PkgDetails where <*> o .:? "failCnt" <*> o .:? "buildTime" <*> fmap parseVersion (o .:? "ghcId") + <*> o .: "runTests" where parseVersion :: Maybe String -> Maybe Version parseVersion Nothing = Nothing diff --git a/src/Distribution/Server/Features/BuildReports/BuildReports.hs b/src/Distribution/Server/Features/BuildReports/BuildReports.hs index ae6d724ab..fa650eda5 100644 --- a/src/Distribution/Server/Features/BuildReports/BuildReports.hs +++ b/src/Distribution/Server/Features/BuildReports/BuildReports.hs @@ -4,25 +4,30 @@ module Distribution.Server.Features.BuildReports.BuildReports ( BuildReport(..), BuildReports(..), + BuildReports_v3, BuildReportId(..), PkgBuildReports(..), BuildLog(..), + TestLog(..), BuildCovg(..), BuildStatus(..), - addRptLogCovg, + addRptLogTestCovg, lookupReportCovg, emptyReports, emptyPkgReports, addReport, deleteReport, setBuildLog, + setTestLog, lookupReport, lookupPackageReports, unsafeSetReport, setFailStatus, resetFailCount, lookupLatestReport, - lookupFailCount + lookupFailCount, + lookupRunTests, + setRunTests ) where import qualified Distribution.Server.Framework.BlobStorage as BlobStorage @@ -48,6 +53,7 @@ import Data.SafeCopy import Data.Typeable (Typeable) import qualified Data.List as L import qualified Data.Char as Char +import Data.Maybe (fromMaybe) import Text.StringTemplate (ToSElem(..)) @@ -77,27 +83,32 @@ instance Parsec BuildReportId where newtype BuildLog = BuildLog BlobStorage.BlobId deriving (Eq, Typeable, Show, MemSize) +newtype TestLog = TestLog BlobStorage.BlobId + deriving (Eq, Typeable, Show, MemSize) data PkgBuildReports = PkgBuildReports { -- for each report, other useful information: Maybe UserId, UTCTime -- perhaps deserving its own data structure (SubmittedReport?) -- When a report was submitted is very useful information. -- also, use IntMap instead of Map BuildReportId? - reports :: !(Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg )), + reports :: !(Map BuildReportId (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg )), -- one more than the maximum report id used nextReportId :: !BuildReportId, - buildStatus :: !BuildStatus + buildStatus :: !BuildStatus, + runTests :: !Bool } deriving (Eq, Typeable, Show) data BuildReports = BuildReports { reportsIndex :: !(Map.Map PackageId PkgBuildReports) + } deriving (Eq, Typeable, Show) emptyPkgReports :: PkgBuildReports emptyPkgReports = PkgBuildReports { reports = Map.empty, nextReportId = BuildReportId 1, - buildStatus = BuildFailCnt 0 + buildStatus = BuildFailCnt 0, + runTests = True } emptyReports :: BuildReports @@ -105,36 +116,38 @@ emptyReports = BuildReports { reportsIndex = Map.empty } -lookupReport :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog) +lookupReport :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog, Maybe TestLog) lookupReport pkgid reportId buildReports = remCvg.Map.lookup reportId . reports =<< Map.lookup pkgid (reportsIndex buildReports) where remCvg Nothing = Nothing - remCvg (Just (brpt,blog,_)) = Just (brpt,blog) + remCvg (Just (brpt,blog,btest,_)) = Just (brpt,blog,btest) -lookupPackageReports :: PackageId -> BuildReports -> [(BuildReportId, (BuildReport, Maybe BuildLog))] +lookupPackageReports :: PackageId -> BuildReports -> [(BuildReportId, (BuildReport, Maybe BuildLog, Maybe TestLog))] lookupPackageReports pkgid buildReports = case Map.lookup pkgid (reportsIndex buildReports) of Nothing -> [] Just rs -> map removeCovg $ Map.toList (reports rs) where - removeCovg (brid,(brpt,blog,_)) = (brid,(brpt,blog)) + removeCovg (brid,(brpt,blog,btest,_)) = (brid,(brpt,blog,btest)) ------------------------- -- PackageIds should /not/ have empty Versions. Caller should ensure this. -addReport :: PackageId -> (BuildReport, Maybe BuildLog) -> BuildReports -> (BuildReports, BuildReportId) -addReport pkgid (brpt,blog) buildReports = +addReport :: PackageId -> (BuildReport, Maybe BuildLog, Maybe TestLog) -> BuildReports -> (BuildReports, BuildReportId) +addReport pkgid (brpt,blog,btest) buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) reportId = nextReportId pkgReports - pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing) (reports pkgReports) + pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,btest,Nothing) (reports pkgReports) , nextReportId = incrementReportId reportId - , buildStatus = buildStatus pkgReports } + , buildStatus = buildStatus pkgReports + , runTests = runTests pkgReports } in (buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) }, reportId) unsafeSetReport :: PackageId -> BuildReportId -> (BuildReport, Maybe BuildLog) -> BuildReports -> BuildReports unsafeSetReport pkgid reportId (brpt,blog) buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) - pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing) (reports pkgReports) + pkgReports' = PkgBuildReports { reports = Map.insert reportId (brpt,blog,Nothing,Nothing) (reports pkgReports) , nextReportId = max (incrementReportId reportId) (nextReportId pkgReports) - , buildStatus = buildStatus pkgReports } + , buildStatus = buildStatus pkgReports + , runTests = runTests pkgReports } in buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } deleteReport :: PackageId -> BuildReportId -> BuildReports -> Maybe BuildReports @@ -150,19 +163,28 @@ setBuildLog pkgid reportId buildLog buildReports = case Map.lookup pkgid (report Nothing -> Nothing Just pkgReports -> case Map.lookup reportId (reports pkgReports) of Nothing -> Nothing - Just (rlog, _, covg) -> let pkgReports' = pkgReports { reports = Map.insert reportId (rlog, buildLog, covg) (reports pkgReports) } + Just (rlog, _, btest, covg) -> let pkgReports' = pkgReports { reports = Map.insert reportId (rlog, buildLog, btest, covg) (reports pkgReports) } in Just $ buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } -addRptLogCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe BuildCovg ) -> BuildReports -> (BuildReports, BuildReportId) -addRptLogCovg pkgid report buildReports = +setTestLog :: PackageId -> BuildReportId -> Maybe TestLog -> BuildReports -> Maybe BuildReports +setTestLog pkgid reportId testLog buildReports = case Map.lookup pkgid (reportsIndex buildReports) of + Nothing -> Nothing + Just pkgReports -> case Map.lookup reportId (reports pkgReports) of + Nothing -> Nothing + Just (rlog, blog, _, covg) -> let pkgReports' = pkgReports { reports = Map.insert reportId (rlog, blog, testLog, covg) (reports pkgReports) } + in Just $ buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } + +addRptLogTestCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) -> BuildReports -> (BuildReports, BuildReportId) +addRptLogTestCovg pkgid report buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) reportId = nextReportId pkgReports pkgReports' = PkgBuildReports { reports = Map.insert reportId report (reports pkgReports) , nextReportId = incrementReportId reportId - , buildStatus = buildStatus pkgReports } + , buildStatus = buildStatus pkgReports + , runTests = runTests pkgReports } in (buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) }, reportId) -lookupReportCovg :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog, Maybe BuildCovg ) +lookupReportCovg :: PackageId -> BuildReportId -> BuildReports -> Maybe (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) lookupReportCovg pkgid reportId buildReports = Map.lookup reportId . reports =<< Map.lookup pkgid (reportsIndex buildReports) setFailStatus :: PackageId -> Bool -> BuildReports -> BuildReports @@ -170,7 +192,8 @@ setFailStatus pkgid fStatus buildReports = let pkgReports = Map.findWithDefault emptyPkgReports pkgid (reportsIndex buildReports) pkgReports' = PkgBuildReports { reports = (reports pkgReports) , nextReportId = (nextReportId pkgReports) - , buildStatus = (getfst fStatus (buildStatus pkgReports)) } + , buildStatus = (getfst fStatus (buildStatus pkgReports)) + , runTests = runTests pkgReports } in buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } where getfst nfst cfst = do @@ -185,7 +208,8 @@ resetFailCount pkgid buildReports = case Map.lookup pkgid (reportsIndex buildRep Just pkgReports -> do let pkgReports' = PkgBuildReports { reports = (reports pkgReports) , nextReportId = (nextReportId pkgReports) - , buildStatus = BuildFailCnt 0 } + , buildStatus = BuildFailCnt 0 + , runTests = runTests pkgReports } return buildReports { reportsIndex = Map.insert pkgid pkgReports' (reportsIndex buildReports) } lookupFailCount :: PackageId -> BuildReports -> Maybe BuildStatus @@ -193,15 +217,23 @@ lookupFailCount pkgid buildReports = do rp <- Map.lookup pkgid (reportsIndex buildReports) return $ buildStatus rp -lookupLatestReport :: PackageId -> BuildReports -> Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg) +lookupLatestReport :: PackageId -> BuildReports -> Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg) lookupLatestReport pkgid buildReports = do rp <- Map.lookup pkgid (reportsIndex buildReports) let rs = reports rp - (maxKey, (rep, buildLog, covg)) <- + (maxKey, (rep, buildLog, testLog, covg)) <- if Map.null rs then Nothing else Just $ Map.findMax rs - Just (maxKey, rep, buildLog, covg) + Just (maxKey, rep, buildLog, testLog, covg) + +lookupRunTests :: PackageId -> BuildReports -> Bool +lookupRunTests pkgid buildReports = maybe True runTests $ Map.lookup pkgid (reportsIndex buildReports) + +setRunTests :: PackageId -> Bool -> BuildReports -> Maybe BuildReports +setRunTests pkgid b buildReports = + let rp = fromMaybe emptyPkgReports $ Map.lookup pkgid (reportsIndex buildReports) + in Just $ BuildReports (Map.insert pkgid rp{runTests = b} (reportsIndex buildReports)) -- addPkg::` ------------------- @@ -242,25 +274,48 @@ instance Migrate BuildLog where deriveSafeCopy 2 'extension ''BuildLog +deriveSafeCopy 0 'base ''TestLog + -- note: if the set of report ids is [1, 2, 3], then nextReportId = 4 -- after calling deleteReport for 3, the set is [1, 2] and nextReportId is still 4. -- however, upon importing, nextReportId will = 3, one more than the maximum present -- this is also a problem in ReportsBackup.hs. but it's not a major issue I think. instance SafeCopy PkgBuildReports where - version = 3 + version = 4 kind = extension - putCopy (PkgBuildReports x _ y) = contain $ safePut (x,y) + putCopy (PkgBuildReports x _ y z) = contain $ safePut (x,y,z) getCopy = contain $ mkReports <$> safeGet where - mkReports (rs,f) = PkgBuildReports rs + mkReports (rs,f,b) = PkgBuildReports rs (if Map.null rs then BuildReportId 1 else incrementReportId (fst $ Map.findMax rs)) - f + f b instance MemSize PkgBuildReports where - memSize (PkgBuildReports a b c) = memSize3 a b c + memSize (PkgBuildReports a b c d) = memSize4 a b c d + + +data PkgBuildReports_v3 = PkgBuildReports_v3 { + reports_v3 :: !(Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg )), + nextReportId_v3 :: !BuildReportId, + buildStatus_v3 :: !BuildStatus +} deriving (Eq, Typeable, Show) + +instance SafeCopy PkgBuildReports_v3 where + version = 3 + kind = extension + putCopy (PkgBuildReports_v3 x _ y) = contain $ safePut (x,y) + getCopy = contain $ mkReports <$> safeGet + where + mkReports (rs,f) = PkgBuildReports_v3 rs + (if Map.null rs + then BuildReportId 1 + else incrementReportId (fst $ Map.findMax rs)) + f +instance MemSize PkgBuildReports_v3 where + memSize (PkgBuildReports_v3 a b c) = memSize3 a b c data PkgBuildReports_v2 = PkgBuildReports_v2 { reports_v2 :: !(Map BuildReportId (BuildReport, Maybe BuildLog)), @@ -309,16 +364,25 @@ instance Migrate PkgBuildReports_v2 where . Map.map (\(br, l) -> (migrate (migrate br), fmap migrate l)) -instance Migrate PkgBuildReports where - type MigrateFrom PkgBuildReports = PkgBuildReports_v2 +instance Migrate PkgBuildReports_v3 where + type MigrateFrom PkgBuildReports_v3 = PkgBuildReports_v2 migrate (PkgBuildReports_v2 m n) = - PkgBuildReports (migrateMap m) n BuildOK + PkgBuildReports_v3 (migrateMap m) n BuildOK where migrateMap :: Map BuildReportId (BuildReport, Maybe BuildLog) -> Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg) migrateMap = Map.mapKeys (\x->x) . Map.map (\(br, l) -> (br, l, Nothing)) +instance Migrate PkgBuildReports where + type MigrateFrom PkgBuildReports = PkgBuildReports_v3 + migrate (PkgBuildReports_v3 m n o) = + PkgBuildReports (migrateMap m) n o True + where + migrateMap :: Map BuildReportId (BuildReport, Maybe BuildLog, Maybe BuildCovg) + -> Map BuildReportId (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg) + migrateMap = Map.mapKeys id + . Map.map (\(br, l, c) -> (br, l, Nothing, c)) data BuildReports_v0 = BuildReports_v0 !(Map.Map PackageIdentifier_v0 PkgBuildReports_v0) @@ -345,12 +409,26 @@ instance MemSize BuildReports_v2 where deriveSafeCopy 2 'extension ''BuildReports_v2 -instance Migrate BuildReports where - type MigrateFrom BuildReports = BuildReports_v2 +data BuildReports_v3 = BuildReports_v3 + { reportsIndex_v3 :: !(Map.Map PackageId PkgBuildReports_v3) + } deriving (Eq, Typeable, Show) + +instance Migrate BuildReports_v3 where + type MigrateFrom BuildReports_v3 = BuildReports_v2 migrate (BuildReports_v2 m) = + BuildReports_v3 (Map.mapKeys id $ Map.map migrate m) + +instance MemSize BuildReports_v3 where + memSize (BuildReports_v3 a) = memSize1 a + +deriveSafeCopy 3 'extension ''BuildReports_v3 + +instance Migrate BuildReports where + type MigrateFrom BuildReports = BuildReports_v3 + migrate (BuildReports_v3 m) = BuildReports (Map.mapKeys id $ Map.map migrate m) instance MemSize BuildReports where memSize (BuildReports a) = memSize1 a -deriveSafeCopy 3 'extension ''BuildReports +deriveSafeCopy 4 'extension ''BuildReports diff --git a/src/Distribution/Server/Features/BuildReports/State.hs b/src/Distribution/Server/Features/BuildReports/State.hs index 0895a95a7..0dec1518c 100644 --- a/src/Distribution/Server/Features/BuildReports/State.hs +++ b/src/Distribution/Server/Features/BuildReports/State.hs @@ -5,7 +5,7 @@ module Distribution.Server.Features.BuildReports.State where import Distribution.Server.Features.BuildReports.BuildReports - (BuildReportId, BuildLog, BuildReport, BuildReports,BuildCovg, BuildStatus) + (BuildReportId, BuildLog, TestLog, BuildReport, BuildReports,BuildCovg, BuildStatus) import qualified Distribution.Server.Features.BuildReports.BuildReports as BuildReports import Distribution.Package @@ -19,9 +19,9 @@ initialBuildReports = BuildReports.emptyReports -- and defined methods addReport :: PackageId -> (BuildReport, Maybe BuildLog) -> Update BuildReports BuildReportId -addReport pkgid report = do +addReport pkgid (bRpt, blog) = do buildReports <- State.get - let (reports, reportId) = BuildReports.addReport pkgid report buildReports + let (reports, reportId) = BuildReports.addReport pkgid (bRpt, blog, Nothing) buildReports State.put reports return reportId @@ -39,10 +39,10 @@ deleteReport pkgid reportId = do Nothing -> return False Just reports -> State.put reports >> return True -lookupReport :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog)) +lookupReport :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog, Maybe TestLog)) lookupReport pkgid reportId = asks (BuildReports.lookupReport pkgid reportId) -lookupPackageReports :: PackageId -> Query BuildReports [(BuildReportId, (BuildReport, Maybe BuildLog))] +lookupPackageReports :: PackageId -> Query BuildReports [(BuildReportId, (BuildReport, Maybe BuildLog, Maybe TestLog))] lookupPackageReports pkgid = asks (BuildReports.lookupPackageReports pkgid) getBuildReports :: Query BuildReports BuildReports @@ -52,13 +52,13 @@ replaceBuildReports :: BuildReports -> Update BuildReports () replaceBuildReports = State.put addRptLogCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe BuildCovg ) -> Update BuildReports BuildReportId -addRptLogCovg pkgid report = do +addRptLogCovg pkgid (bRpt, blog, bcovg) = do buildReports <- State.get - let (reports, reportId) = BuildReports.addRptLogCovg pkgid report buildReports + let (reports, reportId) = BuildReports.addRptLogTestCovg pkgid (bRpt, blog, Nothing, bcovg) buildReports State.put reports return reportId -lookupReportCovg :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog, Maybe BuildCovg)) +lookupReportCovg :: PackageId -> BuildReportId -> Query BuildReports (Maybe (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg)) lookupReportCovg pkgid reportId = asks (BuildReports.lookupReportCovg pkgid reportId) setFailStatus :: PackageId -> Bool -> Update BuildReports () @@ -77,9 +77,33 @@ resetFailCount pkgid = do lookupFailCount :: PackageId -> Query BuildReports (Maybe BuildStatus) lookupFailCount pkgid = asks (BuildReports.lookupFailCount pkgid) -lookupLatestReport :: PackageId -> Query BuildReports (Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe BuildCovg)) +lookupLatestReport :: PackageId -> Query BuildReports (Maybe (BuildReportId, BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg)) lookupLatestReport pkgid = asks (BuildReports.lookupLatestReport pkgid) +addRptLogTestCovg :: PackageId -> (BuildReport, Maybe BuildLog, Maybe TestLog, Maybe BuildCovg ) -> Update BuildReports BuildReportId +addRptLogTestCovg pkgid (bRpt, blog, btest, bcovg) = do + buildReports <- State.get + let (reports, reportId) = BuildReports.addRptLogTestCovg pkgid (bRpt, blog, btest, bcovg) buildReports + State.put reports + return reportId + +setTestLog :: PackageId -> BuildReportId -> Maybe TestLog -> Update BuildReports Bool +setTestLog pkgid reportId testLog = do + buildReports <- State.get + case BuildReports.setTestLog pkgid reportId testLog buildReports of + Nothing -> return False + Just reports -> State.put reports >> return True + +lookupRunTests :: PackageId -> Query BuildReports (Bool) +lookupRunTests pkgid = asks (BuildReports.lookupRunTests pkgid) + +setRunTests :: PackageId -> Bool -> Update BuildReports Bool +setRunTests pkgid b = do + buildReports <- State.get + case BuildReports.setRunTests pkgid b buildReports of + Nothing -> pure False + Just reports -> State.put reports >> pure True + makeAcidic ''BuildReports ['addReport ,'setBuildLog ,'deleteReport @@ -93,5 +117,8 @@ makeAcidic ''BuildReports ['addReport ,'resetFailCount ,'lookupFailCount ,'lookupLatestReport + ,'addRptLogTestCovg + ,'setTestLog + ,'lookupRunTests + ,'setRunTests ] - diff --git a/src/Distribution/Server/Features/Core.hs b/src/Distribution/Server/Features/Core.hs index c6d8835d6..6aec793ab 100644 --- a/src/Distribution/Server/Features/Core.hs +++ b/src/Distribution/Server/Features/Core.hs @@ -18,6 +18,8 @@ module Distribution.Server.Features.Core ( -- * Misc other utils packageExists, packageIdExists, + + packagesStateComponent, ) where -- stdlib diff --git a/src/Distribution/Server/Features/Core/Backup.hs b/src/Distribution/Server/Features/Core/Backup.hs index c46f56da7..bc620a3d9 100644 --- a/src/Distribution/Server/Features/Core/Backup.hs +++ b/src/Distribution/Server/Features/Core/Backup.hs @@ -40,7 +40,7 @@ import qualified Data.Foldable as Foldable import Data.List import Data.List.NonEmpty (toList) import Data.Ord (comparing) -import Control.Monad.State +import Control.Monad import qualified Distribution.Server.Util.GZip as GZip import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy.Char8 as BSC diff --git a/src/Distribution/Server/Features/Documentation.hs b/src/Distribution/Server/Features/Documentation.hs index 98dcfbd81..c73135903 100644 --- a/src/Distribution/Server/Features/Documentation.hs +++ b/src/Distribution/Server/Features/Documentation.hs @@ -1,5 +1,6 @@ {-# LANGUAGE RankNTypes, FlexibleContexts, NamedFieldPuns, RecordWildCards, PatternGuards #-} +{-# LANGUAGE LambdaCase #-} module Distribution.Server.Features.Documentation ( DocumentationFeature(..), DocumentationResource(..), @@ -32,7 +33,9 @@ import Distribution.Package import qualified Distribution.Parsec as P import qualified Data.ByteString.Char8 as C -import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Lazy.Char8 as BSL +import qualified Data.ByteString.Lazy.Search as BSL +import qualified Data.ByteString.Char8 as BS import qualified Data.Map as Map import Data.Function (fix) @@ -41,6 +44,9 @@ import Data.Maybe import Data.Time.Clock (NominalDiffTime, diffUTCTime, getCurrentTime) import System.Directory (getModificationTime) import Control.Applicative +import Distribution.Server.Features.PreferredVersions +import Distribution.Server.Features.PreferredVersions.State (getVersionStatus) +import Distribution.Server.Packages.Types -- TODO: -- 1. Write an HTML view for organizing uploads -- 2. Have cabal generate a standard doc tarball, and serve that here @@ -51,6 +57,8 @@ data DocumentationFeature = DocumentationFeature { queryDocumentation :: forall m. MonadIO m => PackageIdentifier -> m (Maybe BlobId), queryDocumentationIndex :: forall m. MonadIO m => m (Map.Map PackageId BlobId), + latestPackageWithDocumentation :: forall m. MonadIO m => PreferredInfo -> [PkgInfo] -> m (Maybe PackageId), + uploadDocumentation :: DynamicPath -> ServerPartE Response, deleteDocumentation :: DynamicPath -> ServerPartE Response, @@ -82,6 +90,7 @@ initDocumentationFeature :: String -> TarIndexCacheFeature -> ReportsFeature -> UserFeature + -> VersionsFeature -> IO DocumentationFeature) initDocumentationFeature name env@ServerEnv{serverStateDir} = do @@ -91,9 +100,9 @@ initDocumentationFeature name -- Hooks documentationChangeHook <- newHook - return $ \core getPackages upload tarIndexCache reportsCore user -> do + return $ \core getPackages upload tarIndexCache reportsCore user version -> do let feature = documentationFeature name env - core getPackages upload tarIndexCache reportsCore user + core getPackages upload tarIndexCache reportsCore user version documentationState documentationChangeHook return feature @@ -139,6 +148,7 @@ documentationFeature :: String -> TarIndexCacheFeature -> ReportsFeature -> UserFeature + -> VersionsFeature -> StateComponent AcidState Documentation -> Hook PackageId () -> DocumentationFeature @@ -149,13 +159,14 @@ documentationFeature name , guardValidPackageId , corePackagePage , corePackagesPage - , lookupPackageId + , lookupPackageName } getPackages UploadFeature{..} TarIndexCacheFeature{cachedTarIndex} ReportsFeature{..} UserFeature{ guardAuthorised_ } + VersionsFeature{queryGetPreferredInfo} documentationState documentationChangeHook = DocumentationFeature{..} @@ -283,7 +294,13 @@ documentationFeature name let maxAge = documentationCacheTime age ServerTarball.serveTarball (display pkgid ++ " documentation") [{-no index-}] (display pkgid ++ "-docs") - tarball index [Public, maxAge] etag + tarball index [Public, maxAge] etag (Just rewriteDocs) + + rewriteDocs :: BSL.ByteString -> BSL.ByteString + rewriteDocs dochtml = case BSL.breakFindAfter (BS.pack "") dochtml of + ((h,t),True) -> h `BSL.append` extraCss `BSL.append` t + _ -> dochtml + where extraCss = BSL.pack "" -- The cache time for documentation starts at ten minutes and -- increases exponentially for four days, when it cuts off at @@ -352,15 +369,30 @@ documentationFeature name runHook_ documentationChangeHook pkgid noContent (toResponse ()) + latestPackageWithDocumentation :: MonadIO m => PreferredInfo -> [PkgInfo] -> m (Maybe PackageId) + latestPackageWithDocumentation prefInfo ps = helper (reverse ps) + where + helper [] = helper2 (reverse ps) + helper (pkg:pkgs) = do + hasDoc <- queryHasDocumentation (pkgInfoId pkg) + let status = getVersionStatus prefInfo (packageVersion pkg) + if hasDoc && status == NormalVersion + then pure (Just (packageId pkg)) + else helper pkgs + + helper2 [] = pure Nothing + helper2 (pkg:pkgs) = do + hasDoc <- queryHasDocumentation (pkgInfoId pkg) + if hasDoc + then pure (Just (packageId pkg)) + else helper2 pkgs + withDocumentation :: Resource -> DynamicPath -> (PackageId -> BlobId -> TarIndex -> ServerPartE Response) -> ServerPartE Response withDocumentation self dpath func = do pkgid <- packageInPath dpath - -- lookupPackageId returns the latest version if no version is specified. - pkginfo <- lookupPackageId pkgid - -- Set up the canonical URL to point to the unversioned path let basedpath = [ if var == "package" @@ -375,17 +407,27 @@ documentationFeature name -- See https://support.google.com/webmasters/answer/139066?hl=en#6 setHeaderM "Link" canonicalHeader - case pkgVersion pkgid == nullVersion of - -- if no version is given we want to redirect to the latest version - True -> tempRedirect latestPkgPath (toResponse "") - where - latest = packageId pkginfo - dpath' = [ if var == "package" - then (var, display latest) - else e - | e@(var, _) <- dpath ] - latestPkgPath = (renderResource' self dpath') + -- Essentially errNotFound, but overloaded to specify a header. + -- (Needed since errNotFound throws away result of setHeaderM) + let errNotFoundH title message = throwError + (ErrorResponse 404 + [("Link", canonicalHeader)] + title message) + case pkgVersion pkgid == nullVersion of + -- if no version is given we want to redirect to the latest version with docs + True -> do + pkgs <- lookupPackageName (pkgName pkgid) + prefInfo <- queryGetPreferredInfo (pkgName pkgid) + latestPackageWithDocumentation prefInfo pkgs >>= \case + Just latestWithDocs -> do + let dpath' = [ if var == "package" + then (var, display latestWithDocs) + else e + | e@(var, _) <- dpath ] + latestPkgPath = (renderResource' self dpath') + tempRedirect latestPkgPath (toResponse "") + Nothing -> errNotFoundH "Not Found" [MText "There is no documentation for this package."] False -> do mdocs <- queryState documentationState $ LookupDocumentation pkgid case mdocs of @@ -397,13 +439,6 @@ documentationFeature name , MLink canonicalLink canonicalLink , MText " for the latest version." ] - where - -- Essentially errNotFound, but overloaded to specify a header. - -- (Needed since errNotFound throws away result of setHeaderM) - errNotFoundH title message = throwError - (ErrorResponse 404 - [("Link", canonicalHeader)] - title message) Just blob -> do index <- liftIO $ cachedTarIndex blob func pkgid blob index @@ -439,6 +474,7 @@ checkDocTarball pkgid = docMetaPath = DocMeta.packageDocMetaTarPath pkgid + {------------------------------------------------------------------------------ Auxiliary ------------------------------------------------------------------------------} diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 7155f62e4..694093dc5 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -23,7 +23,7 @@ import Distribution.Server.Features.DownloadCount import Distribution.Server.Features.Votes import Distribution.Server.Features.Search import Distribution.Server.Features.PreferredVersions --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies +import Distribution.Server.Features.ReverseDependencies import Distribution.Server.Features.PackageContents (PackageContentsFeature(..)) import Distribution.Server.Features.PackageList import Distribution.Server.Features.Tags @@ -51,7 +51,7 @@ import qualified Distribution.Server.Pages.PackageFromTemplate as PagesNew import Distribution.Server.Pages.Template import Distribution.Server.Pages.Util import qualified Distribution.Server.Pages.Group as Pages --- [reverse index disabled] import qualified Distribution.Server.Pages.Reverse as Pages +import Distribution.Server.Pages.Reverse (LatestOrOld(..), ReverseHtmlUtil(..), reverseHtmlUtil) import qualified Distribution.Server.Pages.Index as Pages import Distribution.Server.Util.CountingMap (cmFind, cmToList) import Distribution.Server.Util.DocMeta (loadTarDocMeta) @@ -76,6 +76,12 @@ import qualified Text.XHtml.Strict as XHtml import Text.XHtml.Table (simpleTable) import Distribution.PackageDescription (hasLibs) import Distribution.PackageDescription.Configuration (flattenPackageDescription) +import qualified Distribution.Server.Pages.Recent as Pages +import qualified Distribution.Server.Util.Paging as Paging +import Distribution.Server.Features.RecentPackages (RecentPackagesFeature (RecentPackagesFeature, getRecentRevisions, getRecentPackages)) +import Data.Time (getCurrentTime) +import Text.Read (readMaybe) +import Distribution.Server.Pages.Group (listGroupCompact) -- TODO: move more of the below to Distribution.Server.Pages.*, it's getting @@ -102,7 +108,7 @@ initHtmlFeature :: ServerEnv -> PackageContentsFeature -> UploadFeature -> PackageCandidatesFeature -> VersionsFeature - -- [reverse index disabled] -> ReverseFeature + -> ReverseFeature -> TagsFeature -> AnalyticsPixelsFeature -> DownloadFeature @@ -114,6 +120,7 @@ initHtmlFeature :: ServerEnv -> TarIndexCacheFeature -> ReportsFeature -> UserDetailsFeature + -> RecentPackagesFeature -> IO HtmlFeature) initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, @@ -123,7 +130,7 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, templates <- loadTemplates serverTemplatesMode [serverTemplatesDir, serverTemplatesDir "Html"] [ "maintain.html", "maintain-candidate.html" - , "reports.html", "report.html" + , "reports.html", "report.html", "reports-test.html" , "maintain-docs.html" , "distro-monitor.html" , "revisions.html" @@ -136,13 +143,14 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, , "noscript-search-form.html" , "analytics-pixels-page.html" , "user-analytics-pixels-page.html" + , "graph.html" ] return $ \user core@CoreFeature{packageChangeHook} packages upload candidates versions - -- [reverse index disabled] reverse + reversef tags analyticsPixels download rank list@ListFeature{itemUpdate} @@ -151,12 +159,14 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, docsCore docsCandidates tarIndexCache reportsCore - usersdetails -> do + usersdetails + recentPackagesFeature -> do -- do rec, tie the knot rec let (feature, packageIndex, packagesPage) = htmlFeature env user core packages upload candidates versions + reversef tags analyticsPixels download rank list names @@ -166,8 +176,10 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, reportsCore usersdetails (htmlUtilities core candidates tags user) + (reverseHtmlUtil reversef) mainCache namesCache templates + recentPackagesFeature -- Index page caches mainCache <- newAsyncCacheNF packageIndex @@ -201,6 +213,7 @@ htmlFeature :: ServerEnv -> UploadFeature -> PackageCandidatesFeature -> VersionsFeature + -> ReverseFeature -> TagsFeature -> AnalyticsPixelsFeature -> DownloadFeature @@ -215,9 +228,11 @@ htmlFeature :: ServerEnv -> ReportsFeature -> UserDetailsFeature -> HtmlUtilities + -> ReverseHtmlUtil -> AsyncCache Response -> AsyncCache Response -> Templates + -> RecentPackagesFeature -> (HtmlFeature, IO Response, IO Response) htmlFeature env@ServerEnv{..} @@ -225,7 +240,7 @@ htmlFeature env@ServerEnv{..} core@CoreFeature{queryGetPackageIndex} packages upload candidates versions - -- [reverse index disabled] ReverseFeature{..} + revf@ReverseFeature{..} tags analyticsPixels download rank list@ListFeature{getAllLists} @@ -236,8 +251,10 @@ htmlFeature env@ServerEnv{..} reportsCore usersdetails utilities@HtmlUtilities{..} + reverseH@ReverseHtmlUtil{..} cachePackagesPage cacheNamesPage templates + recentPackagesFeature = (HtmlFeature{..}, packageIndex, packagesPage) where htmlFeatureInterface = (emptyHackageFeature "html") { @@ -273,22 +290,26 @@ htmlFeature env@ServerEnv{..} distros packages htmlTags + htmlReverse + revf htmlPreferred cachePackagesPage cacheNamesPage templates names candidates + recentPackagesFeature htmlUsers = mkHtmlUsers user usersdetails htmlUploads = mkHtmlUploads utilities upload htmlDocUploads = mkHtmlDocUploads utilities core docsCore templates htmlDownloads = mkHtmlDownloads utilities download - htmlReports = mkHtmlReports utilities core reportsCore templates - htmlCandidates = mkHtmlCandidates utilities core versions upload + htmlReports = mkHtmlReports utilities core upload user reportsCore templates + htmlCandidates = mkHtmlCandidates env utilities core versions upload docsCandidates tarIndexCache candidates user templates htmlPreferred = mkHtmlPreferred utilities core versions htmlTags = mkHtmlTags utilities core upload user list tags templates + htmlReverse = mkHtmlReverse utilities core versions list revf reverseH htmlAnalyticsPixels = mkHtmlAnalyticsPixels utilities core user upload analyticsPixels templates @@ -303,6 +324,7 @@ htmlFeature env@ServerEnv{..} , htmlDownloadsResources htmlDownloads , htmlTagsResources htmlTags , htmlAnalyticsPixelsResources htmlAnalyticsPixels + , htmlReverseResource htmlReverse -- and user groups. package maintainers, trustees, admins , htmlGroupResource user (maintainersGroupResource . uploadResource $ upload) , htmlGroupResource user (trusteesGroupResource . uploadResource $ upload) @@ -338,74 +360,6 @@ htmlFeature env@ServerEnv{..} } -} - - -- reverse index (disabled) - {- - , (extendResource $ reversePackage reverses) { - resourceGet = [("html", serveReverse True)] - } - , (extendResource $ reversePackageOld reverses) { - resourceGet = [("html", serveReverse False)] - } - , (extendResource $ reversePackageAll reverses) { - resourceGet = [("html", serveReverseFlat)] - } - , (extendResource $ reversePackageStats reverses) { - resourceGet = [("html", serveReverseStats)] - } - , (extendResource $ reversePackages reverses) { - resourceGet = [("html", serveReverseList)] - } - -} - - - - -- [reverse index disabled] reverses = reverseResource - - - - - - - {- [reverse index disabled] - -------------------------------------------------------------------------------- - -- Reverse - serveReverse :: Bool -> DynamicPath -> ServerPart Response - serveReverse isRecent dpath = - htmlResponse $ - withPackageId dpath $ \pkgid -> do - let pkgname = packageName pkgid - rdisp <- case packageVersion pkgid of - Version [] [] -> withPackageAll pkgname $ \_ -> revPackageName pkgname - _ -> withPackageVersion pkgid $ \_ -> revPackageId pkgid - render <- (if isRecent then renderReverseRecent else renderReverseOld) pkgname rdisp - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependencies ") $ - Pages.reversePackageRender pkgid (corePackageIdUri "") revr isRecent render - - serveReverseFlat :: DynamicPath -> ServerPart Response - serveReverseFlat dpath = htmlResponse $ - withPackageAllPath dpath $ \pkgname _ -> do - revCount <- query $ GetReverseCount pkgname - pairs <- revPackageFlat pkgname - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ "Flattened reverse dependencies") $ - Pages.reverseFlatRender pkgname (corePackageNameUri "") revr revCount pairs - - serveReverseStats :: DynamicPath -> ServerPart Response - serveReverseStats dpath = htmlResponse $ - withPackageAllPath dpath $ \pkgname pkgs -> do - revCount <- query $ GetReverseCount pkgname - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ "Reverse dependency statistics") $ - Pages.reverseStatsRender pkgname (map packageVersion pkgs) (corePackageIdUri "") revr revCount - - serveReverseList :: DynamicPath -> ServerPart Response - serveReverseList _ = do - let revr = reverseResource revs - triple <- sortedRevSummary revs - hackCount <- PackageIndex.indexSize <$> queryGetPackageIndex - return $ toResponse $ Resource.XHtml $ hackagePage "Reverse dependencies" $ - Pages.reversePackagesRender (corePackageNameUri "") revr hackCount triple - -} - -------------------------------------------------------------------------------- -- Additional package indices @@ -468,12 +422,15 @@ mkHtmlCore :: ServerEnv -> DistroFeature -> PackageContentsFeature -> HtmlTags + -> HtmlReverse + -> ReverseFeature -> HtmlPreferred -> AsyncCache Response -> AsyncCache Response -> Templates -> SearchFeature -> PackageCandidatesFeature + -> RecentPackagesFeature -> HtmlCore mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} utilities@HtmlUtilities{..} @@ -495,16 +452,19 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} DistroFeature{queryPackageStatus} PackageContentsFeature{packageRender} HtmlTags{..} + HtmlReverse{..} + ReverseFeature{queryReverseDeps, revJSON} HtmlPreferred{..} cachePackagesPage cacheNamesPage templates SearchFeature{..} PackageCandidatesFeature{..} + RecentPackagesFeature{getRecentPackages, getRecentRevisions} = HtmlCore{..} where candidatesCore = candidatesCoreResource - cores@CoreResource{packageInPath, lookupPackageName, lookupPackageId} = coreResource + cores@CoreResource {packageInPath, lookupPackageName, lookupPackageId} = coreResource versions = versionsResource docs = documentationResource @@ -536,6 +496,16 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , (extendResource searchPackagesResource) { resourceGet = [("html", serveBrowsePage)] } + , (resourceAt "/packages/graph.json" ) { + resourceDesc = [(GET, "Show JSON of package dependency information")] + , resourceGet = [("json", + serveGraphJSON)] + } + , (resourceAt "/packages/graph" ) { + resourceDesc = [(GET, "Show graph of package dependency information")] + , resourceGet = [("html", + serveGraph)] + } , (extendResource $ corePackagesPage cores) { resourceDesc = [(GET, "Show package index")] , resourceGet = [("html", const $ readAsyncCache cachePackagesPage)] @@ -548,8 +518,76 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , (resourceAt "/package/:package/revisions/.:format") { resourceGet = [("html", serveCabalRevisionsPage)] } + , (resourceAt "/packages/recent.:format") { + resourceGet = [("html", serveRecentPage),("rss", serveRecentRSS)] + } + , (resourceAt "/packages/recent/revisions.:format") { + resourceGet = [("html", serveRevisionPage), ("rss", serveRevisionRSS)] + } ] + readParamWithDefaultAndValid :: (Read a, HasRqData m, Monad m, Functor m, Alternative m) => + a -> (a -> Bool) -> String -> m a + readParamWithDefaultAndValid n f queryParam = do + m <- optional (look queryParam) + let parsed = m >>= readMaybe >>= (\x -> if f x then Just x else Nothing) + + return $ fromMaybe n parsed + + lookupPageSize :: (HasRqData m, Monad m, Functor m, Alternative m) => Int -> m Int + lookupPageSize def = readParamWithDefaultAndValid def validPageSize "pageSize" + where validPageSize x = x > 1 && x <= 200 + + lookupPage :: (HasRqData m, Monad m, Functor m, Alternative m) => Int -> m Int + lookupPage def = readParamWithDefaultAndValid def validPage "page" + where validPage = (>= 1) + + serveRecentPage :: DynamicPath -> ServerPartE Response + serveRecentPage _ = do + recentPackages <- getRecentPackages + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 20 + + let conf = Paging.createConf page pageSize recentPackages + + return . toResponse $ Pages.recentPage conf users recentPackages + + serveRecentRSS :: DynamicPath -> ServerPartE Response + serveRecentRSS _ = do + recentPackages <- getRecentPackages + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 20 + now <- liftIO getCurrentTime + + let conf = Paging.createConf page pageSize recentPackages + + return . toResponse $ Pages.recentFeed conf users serverBaseURI now recentPackages + + serveRevisionPage :: DynamicPath -> ServerPartE Response + serveRevisionPage _ = do + revisions <- getRecentRevisions + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 40 + + let conf = Paging.createConf page pageSize revisions + + return . toResponse $ Pages.revisionsPage conf users revisions + + serveRevisionRSS :: DynamicPath -> ServerPartE Response + serveRevisionRSS _ = do + revisions <- getRecentRevisions + users <- queryGetUserDb + page <- lookupPage 1 + pageSize <- lookupPageSize 40 + now <- liftIO getCurrentTime + + let conf = Paging.createConf page pageSize revisions + + return . toResponse $ Pages.recentRevisionsFeed conf users serverBaseURI now revisions + serveBrowsePage :: DynamicPath -> ServerPartE Response serveBrowsePage _dpath = do template <- getTemplate templates "browse.html" @@ -574,6 +612,19 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} <> noscriptFormRendered ] + serveGraphJSON :: DynamicPath -> ServerPartE Response + serveGraphJSON _ = do + graph <- liftIO revJSON + --TODO: use proper type for graph with ETag + cacheControl [Public, maxAgeMinutes 30] (etagFromHash graph) + ok . toResponse $ graph + + serveGraph :: DynamicPath -> ServerPartE Response + serveGraph _ = do + cacheControlWithoutETag [Public, maxAgeDays 1] -- essentially static + template <- getTemplate templates "graph.html" + return $ toResponse $ template [] + -- Currently the main package page is thrown together by querying a bunch -- of features about their attributes for the given package. It'll need -- reorganizing to look aesthetic, as opposed to the sleek and simple current @@ -590,6 +641,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} docURL = packageDocsContentUri docs realpkg execs = rendExecNames render pkgdesc = flattenPackageDescription $ pkgDesc pkg + maintainers = maintainersGroup pkgname prefInfo <- queryGetPreferredInfo pkgname distributions <- queryPackageStatus pkgname @@ -601,9 +653,12 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} userRating <- case auth of Just (uid,_) -> pkgUserVote pkgname uid; _ -> return Nothing mdoctarblob <- queryDocumentation realpkg tags <- queryTagsForPackage pkgname + rdeps <- queryReverseDeps pkgname deprs <- queryGetDeprecatedFor pkgname mreadme <- makeReadme render hasDocs <- queryHasDocumentation documentationFeature realpkg + mDocPkgId <- if hasDocs then pure Nothing + else latestPackageWithDocumentation documentationFeature prefInfo pkgs rptStats <- queryLastReportStats reportsFeature realpkg candidates <- lookupCandidateName pkgname buildStatus <- renderBuildStatus @@ -611,6 +666,8 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} mdocIndex <- maybe (return Nothing) (liftM Just . liftIO . cachedTarIndex) mdoctarblob analyticsPixels <- getPackageAnalyticsPixels pkgname + userDb <- queryGetUserDb + maintainerlist <- liftIO $ queryUserGroup maintainers let idAndReport = fmap (\(rptId, rpt, _) -> (rptId, rpt)) rptStats install = getInstall $ fmap (fst &&& BR.installOutcome . snd) idAndReport @@ -644,6 +701,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , "analyticsPixels" $= map analyticsPixelUrl (Set.toList analyticsPixels) , "versions" $= (PagesNew.renderVersion realpkg (classifyVersions prefInfo $ map packageVersion pkgs) infoUrl) + , "isDeprecatedVersion" $= getVersionStatus prefInfo (packageVersion realpkg) == DeprecatedVersion , "totalDownloads" $= totalDown , "hasexecs" $= not (null execs) , "recentDownloads" $= recentDown @@ -652,6 +710,9 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , "hasExecOnly" $= (not . hasLibs) pkgdesc && (not . null) execs , "userRating" $= userRating , "score" $= pkgScore + , "hasrdeps" $= not (rdeps == ([],[])) + , "rdeps" $= renderPkgPageDeps rdeps + , "rdepsummary" $= renderDeps pkgname rdeps , "buildStatus" $= buildStatus , "hasDocs" $= hasDocs , "install" $= install @@ -660,11 +721,12 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , "candidates" $= case candidates of [] -> [ toHtml "No Candidates"] _ -> [ PagesNew.commaList $ flip map candidates $ \cand -> anchor ! [href $ corePackageIdUri candidatesCore "" $ packageId cand] << display (packageVersion cand) ] + , "maintainers" $= listGroupCompact (map (Users.userIdToName userDb) (Group.toList maintainerlist)) ] ++ -- Items not related to IO (mostly pure functions) PagesNew.packagePageTemplate render mdocIndex mdocMeta mreadme - docURL distributions + docURL mDocPkgId distributions deprs utilities False @@ -1014,10 +1076,10 @@ data HtmlReports = HtmlReports { htmlReportsResources :: [Resource] } -mkHtmlReports :: HtmlUtilities -> CoreFeature -> ReportsFeature -> Templates -> HtmlReports -mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = HtmlReports{..} +mkHtmlReports :: HtmlUtilities -> CoreFeature -> UploadFeature -> UserFeature -> ReportsFeature -> Templates -> HtmlReports +mkHtmlReports HtmlUtilities{..} CoreFeature{..} UploadFeature{..} UserFeature{..} ReportsFeature{..} templates = HtmlReports{..} where - CoreResource{packageInPath} = coreResource + CoreResource{packageInPath, guardValidPackageId} = coreResource ReportsResource{..} = reportsResource htmlReportsResources = [ @@ -1027,6 +1089,9 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = H , (extendResource reportsPage) { resourceGet = [ ("html", servePackageReport) ] } + , (extendResource reportsTestsEnabled) { + resourceGet = [ ("html", servePackageReportTests) ] + } ] servePackageReports :: DynamicPath -> ServerPartE Response @@ -1048,8 +1113,9 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = H servePackageReport :: DynamicPath -> ServerPartE Response servePackageReport dpath = do - (repid, report, mlog, covg) <- packageReport dpath + (repid, report, mlog, mtest, covg) <- packageReport dpath mlog' <- traverse queryBuildLog mlog + mtest' <- traverse queryTestLog mtest let covg' = fmap getCvgDet covg pkgid <- packageInPath dpath cacheControlWithoutETag [Public, maxAgeDays 30] @@ -1058,6 +1124,7 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = H [ "pkgid" $= (pkgid :: PackageIdentifier) , "report" $= (repid, report) , "log" $= toMessage <$> mlog' + , "test" $= toMessage <$> mtest' , "covg" $= covg' ] where @@ -1075,6 +1142,18 @@ mkHtmlReports HtmlUtilities{..} CoreFeature{..} ReportsFeature{..} templates = H det (_,0) = (100,0,0) det (a,b) = ((a * 100) `div` b ,a,b) + servePackageReportTests :: DynamicPath -> ServerPartE Response + servePackageReportTests dpath = do + pkgid <- packageInPath dpath + guardValidPackageId pkgid + guardAuthorised_ [InGroup (maintainersGroup (packageName pkgid)), InGroup trusteesGroup] + template <- getTemplate templates "reports-test.html" + runTests <- queryRunTests pkgid + return $ toResponse $ template + [ "pkgid" $= pkgid + , "runTests" $= runTests + ] + {------------------------------------------------------------------------------- Candidates -------------------------------------------------------------------------------} @@ -1083,7 +1162,8 @@ data HtmlCandidates = HtmlCandidates { htmlCandidatesResources :: [Resource] } -mkHtmlCandidates :: HtmlUtilities +mkHtmlCandidates :: ServerEnv + -> HtmlUtilities -> CoreFeature -> VersionsFeature -> UploadFeature @@ -1093,7 +1173,7 @@ mkHtmlCandidates :: HtmlUtilities -> UserFeature -> Templates -> HtmlCandidates -mkHtmlCandidates utilities@HtmlUtilities{..} +mkHtmlCandidates ServerEnv{..} utilities@HtmlUtilities{..} CoreFeature{ coreResource = CoreResource{packageInPath} , queryGetPackageIndex } @@ -1102,7 +1182,7 @@ mkHtmlCandidates utilities@HtmlUtilities{..} DocumentationFeature{documentationResource, queryDocumentation,..} TarIndexCacheFeature{cachedTarIndex} PackageCandidatesFeature{..} - UserFeature{ guardAuthorised, guardAuthorised_ } + UserFeature{ guardAuthorised, guardAuthorised_, queryGetUserDb } templates = HtmlCandidates{..} where candidates = candidatesResource @@ -1252,23 +1332,39 @@ mkHtmlCandidates utilities@HtmlUtilities{..} mdocIndex <- maybe (return Nothing) (liftM Just . liftIO . cachedTarIndex) mdoctarblob - let docURL = packageDocsContentUri docs (packageId cand) mreadme <- makeReadme render + let loadDocMeta + | Just doctarblob <- mdoctarblob + , Just docIndex <- mdocIndex + = loadTarDocMeta + (BlobStorage.filepath serverBlobStore doctarblob) + docIndex + (packageId cand) + | otherwise + = return Nothing + mdocMeta <- loadDocMeta + + let docURL = packageDocsContentUri docs (packageId cand) -- also utilize hasIndexedPackage :: Bool let warningBox = case renderWarnings candRender of [] -> [] warn -> [thediv ! [theclass "candidate-warn"] << [paragraph << strong (toHtml "Warnings:"), unordList warn]] + let maintainers = maintainersGroup pkgname + userDb <- queryGetUserDb + maintainerlist <- liftIO $ queryUserGroup maintainers + return $ toResponse . template $ [ "versions" $= (PagesNew.renderVersion (packageId cand) (classifyVersions prefInfo $ insert version otherVersions) Nothing) , "maintainHtml" $= [maintainHtml] , "warningBox" $= warningBox + , "maintainers" $= listGroupCompact (map (Users.userIdToName userDb) (Group.toList maintainerlist)) ] ++ PagesNew.packagePageTemplate render - mdocIndex Nothing mreadme - docURL [] Nothing + mdocIndex mdocMeta mreadme + docURL Nothing [] Nothing utilities True @@ -1998,3 +2094,97 @@ htmlGroupResource UserFeature{..} r@(GroupResource groupR userR getGroup) = groupDeleteUser group dpath goToList dpath goToList dpath = seeOther (renderResource' (groupResource r) dpath) (toResponse ()) + +{------------------------------------------------------------------------------- + Reverse +-------------------------------------------------------------------------------} +data HtmlReverse = HtmlReverse { + htmlReverseResource :: [Resource] + } + +mkHtmlReverse :: HtmlUtilities + -> CoreFeature + -> VersionsFeature + -> ListFeature + -> ReverseFeature + -> ReverseHtmlUtil + -> HtmlReverse +mkHtmlReverse HtmlUtilities{..} + CoreFeature{ coreResource = CoreResource{ + packageInPath + , lookupPackageName + , corePackageIdUri + , corePackageNameUri + }, + queryGetPackageIndex + } + VersionsFeature{withPackageVersion} + ListFeature{} + ReverseFeature{..} + ReverseHtmlUtil{..} + = HtmlReverse{..} + where + htmlReverseResource = [ + (extendResource $ reversePackage reverseResource) { + resourceGet = [("html", serveReverse OnlyLatest)] + } + , (extendResource $ reversePackageOld reverseResource) { + resourceGet = [("html", serveReverse OnlyOlder)] + } + ,(extendResource $ reversePackageFlat reverseResource) { + resourceGet = [("html", serveReverseFlat)] + } + , (extendResource $ reversePackageVerbose reverseResource) { + resourceGet = [("html", serveReverseVerbose)] + } + , (extendResource $ reversePackages reverseResource) { + resourceGet = [("html", serveReverseList)] + } + ] + + + serveReverse :: LatestOrOld -> DynamicPath -> ServerPartE Response + serveReverse isRecent dpath = do + pkgid <- packageInPath dpath + let pkgname = pkgName pkgid + rdisp <- if nullVersion == packageVersion pkgid + then lookupPackageName pkgname *> revPackageName pkgname + else withPackageVersion pkgid $ \_ -> revPackageId pkgid + render <- (if isRecent == OnlyLatest then renderReverseRecent else renderReverseOld) pkgname rdisp + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependencies ") $ + reversePackageRender pkgid (corePackageIdUri "") isRecent render + + redirectIfVersion uriGen pkgid = + if packageVersion pkgid /= nullVersion + then do + let newUri = uriGen reverseResource "" (packageName pkgid) + seeOther newUri () + else pure () + + serveReverseFlat :: DynamicPath -> ServerPartE Response + serveReverseFlat dpath = do + pkg <- packageInPath dpath + redirectIfVersion reverseFlatUri pkg + let pkgname = pkgName pkg + revCount <- revPackageStats pkgname + pairs <- revPackageFlat pkgname + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Flattened reverse dependencies") $ + reverseFlatRender pkgname (corePackageNameUri "") revCount pairs + + serveReverseVerbose :: DynamicPath -> ServerPartE Response + serveReverseVerbose dpath = do + pkg <- packageInPath dpath + redirectIfVersion reverseVerboseUri pkg + let pkgname = pkgName pkg + pkgids <- lookupPackageName pkgname + revCount <- revPackageStats pkgname + versions <- liftIO $ revForEachVersion pkgname + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependency statistics") $ + reverseVerboseRender pkgname (map packageVersion pkgids) (corePackageIdUri "") revCount versions + + serveReverseList :: DynamicPath -> ServerPartE Response + serveReverseList _ = do + namesWithCounts <- revCountForAllPackages + hackCount <- PackageIndex.indexSize <$> queryGetPackageIndex + return $ toResponse $ Resource.XHtml $ hackagePage "Reverse dependencies" $ + reversePackagesRender (corePackageNameUri "") hackCount namesWithCounts diff --git a/src/Distribution/Server/Features/Html/HtmlUtilities.hs b/src/Distribution/Server/Features/Html/HtmlUtilities.hs index 3c1d616da..d0fd2f44f 100644 --- a/src/Distribution/Server/Features/Html/HtmlUtilities.hs +++ b/src/Distribution/Server/Features/Html/HtmlUtilities.hs @@ -26,6 +26,8 @@ data HtmlUtilities = HtmlUtilities { , makeRow :: PackageItem -> Html , renderTags :: Set Tag -> [Html] , renderReviewTags :: Set Tag -> (Set Tag, Set Tag) -> PackageName -> [Html] + , renderDeps :: PackageName -> ([PackageName], [PackageName]) -> Html + , renderPkgPageDeps :: ([PackageName], [PackageName]) -> Html } htmlUtilities :: CoreFeature -> PackageCandidatesFeature -> TagsFeature -> UserFeature -> HtmlUtilities @@ -46,9 +48,11 @@ htmlUtilities CoreFeature{coreResource} makeRow item = tr << [ td $ itemNameHtml , td $ toHtml $ show $ itemDownloads item , td $ toHtml $ show $ itemVotes item + , td $ toHtml $ show $ itemRevDepsCount item , td $ toHtml $ itemDesc item , td $ " (" +++ renderTags (itemTags item) +++ ")" , td $ toHtml $ formatTime defaultTimeLocale "%F" (itemLastUpload item) + , td $ toHtml $ itemLastVersion item , td $ "" +++ intersperse (toHtml ", ") (map renderUser (itemMaintainer item)) ] where @@ -97,3 +101,22 @@ htmlUtilities CoreFeature{coreResource} cores = coreResource + + renderPkgPageDeps :: ([PackageName], [PackageName])-> Html + renderPkgPageDeps (direct, indirect) = + map toHtml [show (length direct), " direct", ", ", show (length indirect), " indirect "] +++ + thespan ! [thestyle "font-size: small", theclass "revdepdetails"] + << (" [" +++ anchor ! [href ""] << "details" +++ "]") + + renderDeps :: PackageName -> ([PackageName], [PackageName])-> Html + renderDeps pkg (direct, indirect) = + (if null direct then (toHtml "") else summary "Direct" direct) +++ + (if null indirect then (toHtml "") else summary "Indirect" indirect) +++ + detailsLink + where + summary title_ dep = thediv << [ bold (toHtml title_), br + , p << intersperse (toHtml ", ") (map packageNameLink dep) + ] + detailsLink = thespan ! [thestyle "font-size: small"] + << (" [" +++ anchor ! [href detailURL] << "details" +++ "]") + detailURL = "/package/" ++ unPackageName pkg ++ "/reverse" diff --git a/src/Distribution/Server/Features/PackageCandidates.hs b/src/Distribution/Server/Features/PackageCandidates.hs index d222beb3f..4b0e2e819 100644 --- a/src/Distribution/Server/Features/PackageCandidates.hs +++ b/src/Distribution/Server/Features/PackageCandidates.hs @@ -611,7 +611,7 @@ candidatesFeature ServerEnv{serverBlobStore = store} Right (fp, etag, index) -> serveTarball (display (packageId pkg) ++ " candidate source tarball") ["index.html"] (display (packageId pkg)) fp index - [Public, maxAgeMinutes 5] etag + [Public, maxAgeMinutes 5] etag Nothing unpackUtf8 :: BS.ByteString -> String unpackUtf8 = T.unpack diff --git a/src/Distribution/Server/Features/PackageContents.hs b/src/Distribution/Server/Features/PackageContents.hs index 7770ffecf..a3f5a2382 100644 --- a/src/Distribution/Server/Features/PackageContents.hs +++ b/src/Distribution/Server/Features/PackageContents.hs @@ -208,7 +208,7 @@ packageContentsFeature CoreFeature{ coreResource = CoreResource{ Right (fp, etag, index) -> serveTarball (display (packageId pkg) ++ " source tarball") [] (display (packageId pkg)) fp index - [Public, maxAgeDays 30] etag + [Public, maxAgeDays 30] etag Nothing unpackUtf8 :: BS.ByteString -> String unpackUtf8 = T.unpack diff --git a/src/Distribution/Server/Features/PackageInfoJSON.hs b/src/Distribution/Server/Features/PackageInfoJSON.hs index 6fdd96e36..3daf2f1ef 100644 --- a/src/Distribution/Server/Features/PackageInfoJSON.hs +++ b/src/Distribution/Server/Features/PackageInfoJSON.hs @@ -53,6 +53,7 @@ import Distribution.Utils.ShortText (fromShortText) import Data.Foldable (toList) import Data.Traversable (for) import qualified Data.List as List +import Data.Time (UTCTime) data PackageInfoJSONFeature = PackageInfoJSONFeature { @@ -130,12 +131,14 @@ initPackageInfoJSONFeature env = do -- | Pure function for extracting basic package info from a Cabal file getBasicDescription - :: CabalFileText + :: UTCTime + -- ^ Time of upload + -> CabalFileText -> Int -- ^ Metadata revision. This will be added to the resulting -- @PackageBasicDescription@ -> Either String PackageBasicDescription -getBasicDescription (CabalFileText cf) metadataRev = +getBasicDescription uploadedAt (CabalFileText cf) metadataRev = let parseResult = PkgDescr.parseGenericPackageDescription (BS.toStrict cf) in case PkgDescr.runParseResult parseResult of (_, Right pkg) -> let @@ -148,6 +151,7 @@ getBasicDescription (CabalFileText cf) metadataRev = PkgDescr.licenseRaw pkgd pbd_homepage = T.pack . fromShortText $ PkgDescr.homepage pkgd pbd_metadata_revision = metadataRev + pbd_uploaded_at = uploadedAt in return $ PackageBasicDescription {..} (_, Left (_, perrs)) -> @@ -201,6 +205,7 @@ servePackageBasicDescription resource preferred packageInfoState dpath = do pkg <- lookupPackageId resource pkgid let metadataRevs = fst <$> pkgMetadataRevisions pkg + uploadInfos = snd <$> pkgMetadataRevisions pkg nMetadata = Vector.length metadataRevs metadataInd = fromMaybe (nMetadata - 1) metadataRev @@ -212,7 +217,8 @@ servePackageBasicDescription resource preferred packageInfoState dpath = do ) let cabalFile = metadataRevs Vector.! metadataInd - pkgDescr = getBasicDescription cabalFile metadataInd + uploadedAt = fst $ uploadInfos Vector.! metadataInd + pkgDescr = getBasicDescription uploadedAt cabalFile metadataInd case pkgDescr of Left e -> Framework.errInternalError [Framework.MText e] Right d -> return d diff --git a/src/Distribution/Server/Features/PackageInfoJSON/State.hs b/src/Distribution/Server/Features/PackageInfoJSON/State.hs index 54227d206..53adfa242 100644 --- a/src/Distribution/Server/Features/PackageInfoJSON/State.hs +++ b/src/Distribution/Server/Features/PackageInfoJSON/State.hs @@ -23,7 +23,7 @@ import Data.Monoid (Sum(..)) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.SafeCopy (SafeCopy(..), base, contain, - deriveSafeCopy) + deriveSafeCopy, safeGet, safePut) import Data.Serialize (Get, get, getListOf, getTwoOf, put, putListOf, putTwoOf) import Data.Typeable (Typeable) @@ -31,6 +31,7 @@ import Data.Word (Word8) import Distribution.License (licenseToSPDX) import Distribution.Text (display, simpleParse) import GHC.Generics (Generic) +import Data.Time (UTCTime) import Distribution.SPDX.License (License) import Distribution.Package (PackageIdentifier, PackageName) @@ -40,8 +41,7 @@ import qualified Distribution.Parsec as Parsec import qualified Distribution.Server.Features.PreferredVersions as Preferred import Distribution.Server.Framework.MemSize (MemSize, - memSize, - memSize7) + memSize, memSize8) -- | Basic information about a package. These values are @@ -54,10 +54,10 @@ data PackageBasicDescription = PackageBasicDescription , pbd_author :: !T.Text , pbd_homepage :: !T.Text , pbd_metadata_revision :: !Int + , pbd_uploaded_at :: !UTCTime } deriving (Eq, Show, Generic) instance SafeCopy PackageBasicDescription where - putCopy PackageBasicDescription{..} = contain $ do put (Pretty.prettyShow pbd_license) put $ T.encodeUtf8 pbd_copyright @@ -66,6 +66,7 @@ instance SafeCopy PackageBasicDescription where put $ T.encodeUtf8 pbd_author put $ T.encodeUtf8 pbd_homepage put pbd_metadata_revision + safePut pbd_uploaded_at getCopy = contain $ do licenseStr <- get @@ -78,6 +79,7 @@ instance SafeCopy PackageBasicDescription where pbd_author <- T.decodeUtf8 <$> get pbd_homepage <- T.decodeUtf8 <$> get pbd_metadata_revision <- get + pbd_uploaded_at <- safeGet return PackageBasicDescription{..} @@ -93,6 +95,7 @@ instance Aeson.ToJSON PackageBasicDescription where , Key.fromString "author" .= pbd_author , Key.fromString "homepage" .= pbd_homepage , Key.fromString "metadata_revision" .= pbd_metadata_revision + , Key.fromString "uploaded_at" .= pbd_uploaded_at ] @@ -110,6 +113,7 @@ instance Aeson.FromJSON PackageBasicDescription where pbd_author <- obj .: Key.fromString "author" pbd_homepage <- obj .: Key.fromString "homepage" pbd_metadata_revision <- obj .: Key.fromString "metadata_revision" + pbd_uploaded_at <- obj .: Key.fromString "uploaded_at" return $ PackageBasicDescription {..} @@ -225,8 +229,8 @@ deriveSafeCopy 0 'base ''PackageInfoState instance MemSize PackageBasicDescription where memSize PackageBasicDescription{..} = - memSize7 (Pretty.prettyShow pbd_license) pbd_copyright pbd_synopsis - pbd_description pbd_author pbd_homepage pbd_metadata_revision + memSize8 (Pretty.prettyShow pbd_license) pbd_copyright pbd_synopsis + pbd_description pbd_author pbd_homepage pbd_metadata_revision pbd_uploaded_at instance MemSize PackageVersions where memSize (PackageVersions ps) = getSum $ diff --git a/src/Distribution/Server/Features/PackageList.hs b/src/Distribution/Server/Features/PackageList.hs index 1a719fc22..51cbf5fe0 100644 --- a/src/Distribution/Server/Features/PackageList.hs +++ b/src/Distribution/Server/Features/PackageList.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE RankNTypes, RecordWildCards #-} +{-# LANGUAGE RankNTypes, RecordWildCards, NamedFieldPuns #-} module Distribution.Server.Features.PackageList ( ListFeature(..), initListFeature, @@ -9,7 +9,7 @@ module Distribution.Server.Features.PackageList ( import Distribution.Server.Framework import Distribution.Server.Features.Core --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies +import Distribution.Server.Features.ReverseDependencies import Distribution.Server.Features.Votes import Distribution.Server.Features.DownloadCount import Distribution.Server.Features.Tags @@ -23,15 +23,16 @@ import qualified Distribution.Server.Packages.PackageIndex as PackageIndex import Distribution.Server.Util.CountingMap (cmFind) import Distribution.Server.Packages.Types --- [reverse index disabled] import Distribution.Server.Packages.Reverse import Distribution.Server.Users.Types import Distribution.Package import Distribution.PackageDescription import Distribution.PackageDescription.Configuration +import Distribution.Pretty (prettyShow) import Distribution.Utils.ShortText (fromShortText) import Control.Concurrent +import qualified Data.List.NonEmpty as NE import Data.Maybe (mapMaybe) import Data.Map (Map) import qualified Data.Map as Map @@ -75,7 +76,7 @@ data PackageItem = PackageItem { itemDownloads :: !Int, -- The number of direct revdeps. (Likewise.) -- also: distinguish direct/flat? - -- [reverse index disabled] itemRevDepsCount :: !Int, + itemRevDepsCount :: !Int, -- Whether there's a library here. itemHasLibrary :: !Bool, -- How many executables (>=0) this package has. @@ -85,23 +86,25 @@ data PackageItem = PackageItem { -- How many benchmarks (>=0) this package has. itemNumBenchmarks :: !Int, -- Last upload date - itemLastUpload :: !UTCTime - -- Hotness: a more heuristic way to sort packages. presently non-existent. - --itemHotness :: Int + itemLastUpload :: !UTCTime, + -- Hotness = recent downloads + stars + 2 * no rev deps + itemHotness :: !Float, + -- Last version + itemLastVersion :: !String } instance MemSize PackageItem where - memSize (PackageItem a b c d e f g h i j k l) = memSize12 a b c d e f g h i j k l + memSize (PackageItem a b c d e f g h i j k l _m n o) = memSize11 a b c d e f g h i j (k, l, n, o) emptyPackageItem :: PackageName -> PackageItem emptyPackageItem pkg = PackageItem pkg Set.empty Nothing "" [] - 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) + 0 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) 0 "" initListFeature :: ServerEnv -> IO (CoreFeature - -- [reverse index disabled] -> ReverseFeature + -> ReverseFeature -> DownloadFeature -> VotesFeature -> TagsFeature @@ -114,7 +117,7 @@ initListFeature _env = do itemUpdate <- newHook return $ \core@CoreFeature{..} - -- [reverse index disabled] revs + revs@ReverseFeature{revDirectCount, reverseHook} download votesf@VotesFeature{..} tagsf@TagsFeature{..} @@ -123,7 +126,7 @@ initListFeature _env = do uploads@UploadFeature{..} -> do let (feature, modifyItem, updateDesc) = - listFeature core download votesf tagsf versions users uploads + listFeature core revs download votesf tagsf versions users uploads itemCache itemUpdate registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, _) -> @@ -143,15 +146,15 @@ initListFeature _env = do runHook_ itemUpdate (Set.singleton pkgname) Nothing -> return () - {- [reverse index disabled] - votesf@VotesFeature{..} - registerHook (reverseUpdateHook revs) $ \mrev -> do - let pkgs = Map.keys mrev - forM_ pkgs $ \pkgname -> do - revCount <- query . GetReverseCount $ pkgname - modifyItem pkgname (updateReverseItem revCount) - runHook' itemUpdate $ Set.fromDistinctAscList pkgs - -} + registerHook reverseHook $ \pkginfos -> do + let + names = Set.fromDistinctAscList $ + map (pkgName . pkgInfoId . NE.head) + pkginfos + forM_ names $ \pkgname -> do + revDirect <- revDirectCount pkgname + modifyItem pkgname (updateReverseItem revDirect) + runHook_ itemUpdate names registerHook votesUpdated $ \(pkgname, _) -> do votes <- pkgNumScore pkgname @@ -172,6 +175,7 @@ initListFeature _env = do listFeature :: CoreFeature + -> ReverseFeature -> DownloadFeature -> VotesFeature -> TagsFeature @@ -185,6 +189,7 @@ listFeature :: CoreFeature PackageName -> IO ()) listFeature CoreFeature{..} + ReverseFeature{revDirectCount} DownloadFeature{..} VotesFeature{..} TagsFeature{..} @@ -249,7 +254,8 @@ listFeature CoreFeature{..} constructItem :: PkgInfo -> IO (PackageName, PackageItem) constructItem pkg = do let pkgname = packageName pkg - -- [reverse index disabled] revCount <- query . GetReverseCount $ pkgname + desc = pkgDesc pkg + intRevDirectCount <- revDirectCount pkgname users <- queryGetUserDb tags <- queryTagsForPackage pkgname downs <- recentPackageDownloads @@ -257,14 +263,16 @@ listFeature CoreFeature{..} deprs <- queryGetDeprecatedFor pkgname maintainers <- queryUserGroup (maintainersGroup pkgname) - return $ (,) pkgname $ (updateDescriptionItem (pkgDesc pkg) $ emptyPackageItem pkgname) { + return $ (,) pkgname $ (updateDescriptionItem desc $ emptyPackageItem pkgname) { itemTags = tags , itemMaintainer = map (userIdToName users) (UserIdSet.toList maintainers) , itemDeprecated = deprs , itemDownloads = cmFind pkgname downs - -- [reverse index disabled] , itemRevDepsCount = directReverseCount revCount , itemVotes = votes , itemLastUpload = fst (pkgOriginalUploadInfo pkg) + , itemRevDepsCount = intRevDirectCount + , itemHotness = votes + fromIntegral (cmFind pkgname downs) + fromIntegral intRevDirectCount * 2 + , itemLastVersion = prettyShow $ pkgVersion $ pkgInfoId pkg } ------------------------------ @@ -308,7 +316,8 @@ updateTagItem tags item = updateVoteItem :: Float -> PackageItem -> PackageItem updateVoteItem score item = item { - itemVotes = score + itemVotes = score, + itemHotness = fromIntegral (itemRevDepsCount item) * 2 + score + fromIntegral (itemDownloads item) } updateDeprecation :: Maybe [PackageName] -> PackageItem -> PackageItem @@ -317,16 +326,16 @@ updateDeprecation pkgs item = itemDeprecated = pkgs } -{- [reverse index disabled] -updateReverseItem :: ReverseCount -> PackageItem -> PackageItem -updateReverseItem revCount item = +updateReverseItem :: Int -> PackageItem -> PackageItem +updateReverseItem revDirectCount item = item { - itemRevDepsCount = directReverseCount revCount + itemRevDepsCount = revDirectCount, + itemHotness = fromIntegral revDirectCount * 2 + itemVotes item + fromIntegral (itemDownloads item) } --} updateDownload :: Int -> PackageItem -> PackageItem updateDownload count item = item { - itemDownloads = count + itemDownloads = count, + itemHotness = fromIntegral (itemRevDepsCount item) * 2 + itemVotes item + realToFrac count } diff --git a/src/Distribution/Server/Features/PreferredVersions.hs b/src/Distribution/Server/Features/PreferredVersions.hs index 860c9b7c7..78c5ddeac 100644 --- a/src/Distribution/Server/Features/PreferredVersions.hs +++ b/src/Distribution/Server/Features/PreferredVersions.hs @@ -7,9 +7,11 @@ module Distribution.Server.Features.PreferredVersions ( PreferredInfo(..), VersionStatus(..), + getVersionStatus, classifyVersions, PreferredRender(..), + preferredStateComponent, ) where import Distribution.Server.Framework @@ -48,6 +50,7 @@ data VersionsFeature = VersionsFeature { queryGetPreferredInfo :: forall m. MonadIO m => PackageName -> m PreferredInfo, queryGetDeprecatedFor :: forall m. MonadIO m => PackageName -> m (Maybe [PackageName]), + queryGetPreferredVersions :: forall m. MonadIO m => m PreferredVersions, versionsResource :: VersionsResource, deprecatedHook :: Hook (PackageName, Maybe [PackageName]) (), @@ -60,6 +63,7 @@ data VersionsFeature = VersionsFeature { doPreferredsRender :: forall m. MonadIO m => m [(PackageName, PreferredRender)], doDeprecatedsRender :: forall m. MonadIO m => m [(PackageName, [PackageName])], + withPackageVersion :: forall a. PackageId -> (PkgInfo -> ServerPartE a) -> ServerPartE a, withPackagePreferred :: forall a. PackageId -> (PkgInfo -> [PkgInfo] -> ServerPartE a) -> ServerPartE a, withPackagePreferredPath :: forall a. DynamicPath -> (PkgInfo -> [PkgInfo] -> ServerPartE a) -> ServerPartE a } @@ -156,6 +160,9 @@ versionsFeature ServerEnv{ serverVerbosity = verbosity } queryGetDeprecatedFor :: MonadIO m => PackageName -> m (Maybe [PackageName]) queryGetDeprecatedFor name = queryState preferredState (GetDeprecatedFor name) + queryGetPreferredVersions :: MonadIO m => m PreferredVersions + queryGetPreferredVersions = queryState preferredState GetPreferredVersions + updateDeprecatedTags = do pkgs <- deprecatedMap <$> queryState preferredState GetPreferredVersions setCalculatedTag (Tag "deprecated") (Map.keysSet pkgs) @@ -264,6 +271,17 @@ versionsFeature ServerEnv{ serverVerbosity = verbosity } runHook_ deprecatedHook (pkgname, deprs) updateDeprecatedTags + withPackageVersion :: PackageId -> (PkgInfo -> ServerPartE a) -> ServerPartE a + withPackageVersion pkgid func = do + pkgIndex <- queryGetPackageIndex + guard (packageVersion pkgid /= nullVersion) + case PackageIndex.lookupPackageName pkgIndex (packageName pkgid) of + [] -> packageError [MText $ "No such package in package index. ", MLink "Search for related terms instead?"$ "/packages/search?terms=" ++ (display $ pkgName pkgid)] + pkg -> case find ((== packageVersion pkgid) . packageVersion) pkg of + Nothing -> packageError [MText $ "No such package version for " ++ display (packageName pkgid)] + Just pkg' -> func pkg' + where packageError = errNotFound "Package not found" + --------------------------- -- This is a function used by the HTML feature to select the version to display. -- It could be enhanced by displaying a search page in the case of failure, diff --git a/src/Distribution/Server/Features/RecentPackages.hs b/src/Distribution/Server/Features/RecentPackages.hs index d8e0ec430..51fe5366a 100644 --- a/src/Distribution/Server/Features/RecentPackages.hs +++ b/src/Distribution/Server/Features/RecentPackages.hs @@ -6,25 +6,17 @@ module Distribution.Server.Features.RecentPackages ( ) where import Distribution.Server.Framework - import Distribution.Server.Features.Core import Distribution.Server.Features.Users - import Distribution.Server.Packages.Types - import qualified Distribution.Server.Packages.PackageIndex as PackageIndex -import qualified Distribution.Server.Framework.ResponseContentTypes as Resource - -import Data.Time.Clock (getCurrentTime) -import Data.List (sortBy) -import Data.Ord (comparing) - --- the goal is to have the HTML modules import /this/ one, not the other way around -import qualified Distribution.Server.Pages.Recent as Pages +import Data.List (sortOn) +import Data.Ord (Down(Down)) data RecentPackagesFeature = RecentPackagesFeature { recentPackagesFeatureInterface :: HackageFeature, - recentPackagesResource :: RecentPackagesResource + getRecentPackages :: forall m. MonadIO m => m [PkgInfo], + getRecentRevisions :: forall m. MonadIO m => m [PkgInfo] -- other informational hooks: perhaps a simplified CondTree so a browser script can dynamically change the package page based on flags } @@ -51,9 +43,9 @@ initRecentPackagesFeature env@ServerEnv{serverCacheDelay, serverVerbosity = verb recentPackagesFeature env user core cacheRecent - cacheRecent <- newAsyncCacheNF updateRecentCache + cacheRecent <- newAsyncCacheWHNF updateRecentCache defaultAsyncCachePolicy { - asyncCacheName = "recent uploads and revisions (html,rss,html,rss)", + asyncCacheName = "recent uploads and revisions", asyncCacheUpdateDelay = serverCacheDelay, asyncCacheSyncInit = False, asyncCacheLogVerbosity = verbosity @@ -68,76 +60,40 @@ initRecentPackagesFeature env@ServerEnv{serverCacheDelay, serverVerbosity = verb recentPackagesFeature :: ServerEnv -> UserFeature -> CoreFeature - -> AsyncCache (Response, Response, Response, Response) - -> (RecentPackagesFeature, IO (Response, Response, Response, Response)) + -> AsyncCache ([PkgInfo], [PkgInfo]) + -> (RecentPackagesFeature, IO ([PkgInfo], [PkgInfo])) -recentPackagesFeature env +recentPackagesFeature _ UserFeature{..} CoreFeature{..} cacheRecent = (RecentPackagesFeature{..}, updateRecentCache) where recentPackagesFeatureInterface = (emptyHackageFeature "recentPackages") { - featureResources = map ($ recentPackagesResource) [recentPackages, recentRevisions] - , featureState = [] - , featureCaches = [ + featureState = [], + featureCaches = [ CacheComponent { - cacheDesc = "recents packages and revisions page (html, rss, html, rss)", + cacheDesc = "recent packages and revisions", getCacheMemSize = memSize <$> readAsyncCache cacheRecent } ] - , featurePostInit = syncAsyncCache cacheRecent } - recentPackagesResource = RecentPackagesResource { - recentPackages = (extendResourcePath "/recent.:format" (corePackagesPage coreResource)) { - resourceGet = [ - ("html", const $ liftM (\(x,_,_,_) -> x) $ readAsyncCache cacheRecent) - , ("rss", const $ addAllowOriginHeader >> (liftM (\(_,x,_,_) -> x) $ readAsyncCache cacheRecent)) - ] - }, - recentRevisions = (extendResourcePath "/recent/revisions.:format" (corePackagesPage coreResource)) { - resourceGet = [ - ("html", const $ liftM (\(_,_,x,_) -> x) $ readAsyncCache cacheRecent) - , ("rss", const $ addAllowOriginHeader >> (liftM (\(_,_,_,x) -> x) $ readAsyncCache cacheRecent)) - ] - } - } + getRecentPackages :: MonadIO m => m [PkgInfo] + getRecentPackages = fst <$> readAsyncCache cacheRecent + + getRecentRevisions :: MonadIO m => m [PkgInfo] + getRecentRevisions = snd <$> readAsyncCache cacheRecent + + updateRecentCache :: IO ([PkgInfo], [PkgInfo]) updateRecentCache = do - -- TODO: move the html version to the HTML feature pkgIndex <- queryGetPackageIndex - users <- queryGetUserDb - now <- getCurrentTime - let recentChanges = sortBy (flip $ comparing pkgOriginalUploadTime) - (PackageIndex.allPackages pkgIndex) - xmlRepresentation = toResponse $ Resource.XHtml $ Pages.recentPage users recentChanges - rssRepresentation = toResponse $ Pages.recentFeed users (serverBaseURI env) now recentChanges - - recentRevisions = sortBy (flip $ comparing revisionTime) . - filter isRevised $ (PackageIndex.allPackages pkgIndex) - revisionTime pkgInfo = pkgLatestUploadTime pkgInfo - isRevised pkgInfo = pkgNumRevisions pkgInfo > 1 - xmlRevisions = toResponse $ Resource.XHtml $ Pages.revisionsPage users recentRevisions - rssRevisions = toResponse $ Pages.recentRevisionsFeed users (serverBaseURI env) now recentRevisions - - return (xmlRepresentation, rssRepresentation, xmlRevisions, rssRevisions) - - -addAllowOriginHeader :: (FilterMonad Response m) => m () -addAllowOriginHeader = addHeaderM "Access-Control-Allow-Origin" "*" -{- -data SimpleCondTree = SimpleCondNode [Dependency] [(Condition ConfVar, SimpleCondTree, SimpleCondTree)] - | SimpleCondLeaf - deriving (Show, Eq) + let packages = PackageIndex.allPackages pkgIndex + isRevised pkgInfo = pkgNumRevisions pkgInfo > 1 + revisionTime pkgInfo = pkgLatestUploadTime pkgInfo + recentChanges = sortOn (Down . pkgOriginalUploadTime) packages + recentRevisions = sortOn (Down . revisionTime) . filter isRevised $ packages -doMakeCondTree :: GenericPackageDescription -> [(String, SimpleCondTree)] -doMakeCondTree desc = map (\lib -> ("library", makeCondTree lib)) (maybeToList $ condLibrary desc) - ++ map (\(exec, tree) -> (exec, makeCondTree tree)) (condExecutables desc) - where - makeCondTree (CondNode _ deps comps) = case deps of - [] -> SimpleCondLeaf - _ -> SimpleCondNode deps $ map makeCondComponents comps - makeCondComponents (cond, tree, mtree) = (cond, makeCondTree tree, maybe SimpleCondLeaf makeCondTree mtree) --} + return (recentChanges, recentRevisions) \ No newline at end of file diff --git a/src/Distribution/Server/Features/ReverseDependencies.hs b/src/Distribution/Server/Features/ReverseDependencies.hs index e25eb5c77..bff4c7f8e 100644 --- a/src/Distribution/Server/Features/ReverseDependencies.hs +++ b/src/Distribution/Server/Features/ReverseDependencies.hs @@ -1,247 +1,290 @@ -{-# LANGUAGE RankNTypes, RecordWildCards #-} +{-# LANGUAGE DeriveGeneric, RankNTypes, NamedFieldPuns, RecordWildCards #-} module Distribution.Server.Features.ReverseDependencies ( - ReverseFeature, - reverseResource, + ReverseCount(..), + ReverseFeature(..), + ReversePageRender(..), + ReverseRender(..), ReverseResource(..), - reverseUpdateHook, initReverseFeature, - ReverseRender(..), - ReversePageRender(..), - revPackageId, - revPackageName, - renderReverseRecent, - renderReverseOld, - revPackageFlat, - revPackageStats, - revPackageSummary, - revSummary, - sortedRevSummary + reverseFeature ) where -import Distribution.Server.Acid (query, update) import Distribution.Server.Framework -import Distribution.Server.Framework.BackupRestore -import Distribution.Server.Framework.BackupDump (testRoundtripByQuery) import Distribution.Server.Features.Core import Distribution.Server.Features.PreferredVersions - -import Distribution.Server.Packages.State -import Distribution.Server.Packages.Reverse -import Distribution.Server.Packages.Preferred -import qualified Distribution.Server.Framework.Cache as Cache - +import Distribution.Server.Features.PreferredVersions.State (PreferredVersions) +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Server.Packages.PackageIndex (PackageIndex, packageNames, allPackagesByNameNE) +import Distribution.Server.Packages.Types (PkgInfo) +import Distribution.Server.Features.ReverseDependencies.State import Distribution.Package import Distribution.Text (display) -import Distribution.Version - -import Data.List (mapAccumL, sortBy) -import Data.Maybe (catMaybes) -import Data.Function (fix, on) -import Data.Map (Map) +import Distribution.Version (Version) + +import Data.Aeson +import Data.ByteString.Lazy (ByteString) +import Data.Containers.ListUtils (nubOrd) +import Data.List (mapAccumL, sortOn) +import qualified Data.List.NonEmpty as NE +import Data.Maybe (catMaybes, mapMaybe, fromMaybe) +import Data.Function (fix) +import qualified Data.Bimap as Bimap +import qualified Data.Array as Arr +import qualified Data.Graph as Gr import qualified Data.Map as Map +import Data.Set (Set) import qualified Data.Set as Set -import Control.Monad (liftM, forever) -import Control.Monad.Trans (MonadIO) -import Control.Concurrent (forkIO) -import Control.Concurrent.Chan +import GHC.Generics hiding (packageName) data ReverseFeature = ReverseFeature { reverseFeatureInterface :: HackageFeature, reverseResource :: ReverseResource, - reverseUpdateHook :: Hook (Map PackageName [Version] -> IO ()) + + reverseHook :: Hook [NE.NonEmpty PkgInfo] (), + + queryReverseDeps :: forall m. MonadIO m => PackageName -> m ([PackageName], [PackageName]), + revPackageId :: forall m. MonadIO m => PackageId -> m ReverseDisplay, + revPackageName :: forall m. MonadIO m => PackageName -> m ReverseDisplay, + renderReverseRecent :: forall m. MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender, + renderReverseOld :: forall m. MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender, + revPackageFlat :: forall m. MonadIO m => PackageName -> m [(PackageName, Int)], + revDirectCount :: forall m. MonadIO m => PackageName -> m Int, + revPackageStats :: forall m. MonadIO m => PackageName -> m ReverseCount, + revCountForAllPackages :: forall m. MonadIO m => m [(PackageName, ReverseCount)], + revJSON :: IO ByteString, + revDisplayInfo :: forall m. MonadIO m => m VersionIndex, + revForEachVersion :: PackageName -> IO (Map.Map Version (Set PackageIdentifier)) } instance IsHackageFeature ReverseFeature where getFeatureInterface = reverseFeatureInterface - data ReverseResource = ReverseResource { reversePackage :: Resource, reversePackageOld :: Resource, - reversePackageAll :: Resource, - reversePackageStats :: Resource, + reversePackageFlat :: Resource, + reversePackageVerbose :: Resource, reversePackages :: Resource, - reversePackagesAll :: Resource, reverseUri :: String -> PackageId -> String, reverseNameUri :: String -> PackageName -> String, reverseOldUri :: String -> PackageId -> String, - reverseOldNameUri :: String -> PackageName -> String, - reverseAllUri :: String -> PackageName -> String, - reverseStatsUri :: String -> PackageName -> String, - reversesUri :: String -> String, - reversesAllUri :: String -> String + reverseFlatUri :: String -> PackageName -> String, + reverseVerboseUri :: String -> PackageName -> String } initReverseFeature :: ServerEnv -> IO (CoreFeature + -> VersionsFeature -> IO ReverseFeature) -initReverseFeature ServerEnv{serverVerbosity = verbosity} = do - revChan <- newChan - registerHook (packageAddHook core) $ \pkg -> writeChan revChan $ - update $ AddReversePackage (packageId pkg) (getAllDependencies pkg) - registerHook (packageRemoveHook core) $ \pkg -> writeChan revChan $ - update $ RemoveReversePackage (packageId pkg) (getAllDependencies pkg) - registerHook (packageChangeHook core) $ \pkg pkg' -> writeChan revChan $ - update $ ChangeReversePackage (packageId pkg) - (getAllDependencies pkg) (getAllDependencies pkg') - - revHook <- newHook - let select (_, b, _) = b - sortedRevs = fmap (sortBy $ on (flip compare) select) revSummary - revTopCache <- Cache.newCacheable =<< sortedRevs - registerHook revHook $ \_ -> Cache.putCache revTopCache =<< sortedRevs - - return $ \core -> do - let feature = reverseFeature core - revChan revHook revTopCache +initReverseFeature _ = do + updateReverse <- newHook + + return $ \CoreFeature{queryGetPackageIndex,packageChangeHook} + VersionsFeature{queryGetPreferredVersions} -> do + idx <- queryGetPackageIndex + memState <- newMemStateWHNF $ constructReverseIndex idx + + let feature = reverseFeature queryGetPackageIndex queryGetPreferredVersions memState updateReverse + + registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, mpkginfo) -> + case mpkginfo of + Nothing -> return () --PackageRemoveHook + Just pkginfo -> do + index <- queryGetPackageIndex + modifyMemState memState $ addPackage index (packageName pkgid) (getDepNames pkginfo) + runHook_ updateReverse [pure pkginfo] return feature -reverseFeature :: CoreFeature - -> Chan (IO (Map PackageName [Version])) - -> Hook (Map PackageName [Version] -> IO ()) - -> Cache.Cache [(PackageName, Int, Int)] +data ReverseRender = ReverseRender { + rendRevPkg :: PackageId, + rendRevStatus :: Maybe VersionStatus, + rendRevCount :: Int +} deriving (Show, Eq, Ord) + +data ReversePageRender = ReversePageRender { + rendRevList :: [ReverseRender], + rendFilterCount :: (Int, Int), + rendPageTotal :: Int +} + +-- data Node = Node {id::Int, label::String} deriving Generic +data Edge = Edge { + id::Int, + name::String, + deps::[String] + } deriving Generic +-- data JGraph = JGraph { nodes::[Node], edges::[Edge]} deriving Generic +-- instance ToJSON Node +instance ToJSON Edge +-- instance ToJSON JGraph +-- instance ToJSON PackageName + +reverseFeature :: IO (PackageIndex PkgInfo) + -> IO PreferredVersions + -> MemState ReverseIndex + -> Hook [NE.NonEmpty PkgInfo] () -> ReverseFeature -reverseFeature CoreFeature{..} - reverseStream reverseUpdateHook reverseTopCache +reverseFeature queryGetPackageIndex + queryGetPreferredVersions + reverseMemState + reverseHook = ReverseFeature{..} where reverseFeatureInterface = (emptyHackageFeature "reverse") { - featureResources = map ($reverseResource) [] - , featurePostInit = forkIO transferReverse >> return () - , featureDumpRestore = Just (return [], restoreBackup, testRoundtripByQuery (query GetReverseIndex)) + featureResources = map ($ reverseResource) [] + , featurePostInit = initReverseIndex + , featureState = [] + , featureCaches = [ + CacheComponent { + cacheDesc = "reverse index", + getCacheMemSize = memSize <$> readMemState reverseMemState + } + ] } - transferReverse = forever $ do - revFunc <- readChan reverseStream - modded <- revFunc - runHook' reverseUpdateHook modded - - --TODO: this isn't a restore! - -- do we need a post init/restore hook for initialising caches? - restoreBackup = RestoreBackup - { restoreEntry = \_ -> return $ Right restoreBackup - , restoreFinalize = return $ Right restoreBackup - , restoreComplete = do - putStrLn "Calculating reverse dependencies" - index <- fmap packageList $ query GetPackagesState - let revs = constructReverseIndex index - update $ ReplaceReverseIndex revs - } + initReverseIndex :: IO () + initReverseIndex = do + index <- liftIO queryGetPackageIndex + -- We build the proper index earlier, this just fires the reverse hooks + runHook_ reverseHook $ allPackagesByNameNE index + reverseResource = fix $ \r -> ReverseResource { reversePackage = resourceAt "/package/:package/reverse.:format" , reversePackageOld = resourceAt "/package/:package/reverse/old.:format" - , reversePackageAll = resourceAt "/package/:package/reverse/all.:format" - , reversePackageStats = resourceAt "/package/:package/reverse/summary.:format" + , reversePackageFlat = resourceAt "/package/:package/reverse/flat.:format" + , reversePackageVerbose = resourceAt "/package/:package/reverse/verbose.:format" , reversePackages = resourceAt "/packages/reverse.:format" - , reversePackagesAll = resourceAt "/packages/reverse/all.:format" , reverseUri = \format pkg -> renderResource (reversePackage r) [display pkg, format] , reverseNameUri = \format pkg -> renderResource (reversePackage r) [display pkg, format] , reverseOldUri = \format pkg -> renderResource (reversePackageOld r) [display pkg, format] - , reverseOldNameUri = \format pkg -> renderResource (reversePackageOld r) [display pkg, format] - , reverseAllUri = \format pkg -> renderResource (reversePackageAll r) [display pkg, format] - , reverseStatsUri = \format pkg -> renderResource (reversePackageStats r) [display pkg, format] - , reversesUri = \format -> renderResource (reversePackages r) [format] - , reversesAllUri = \format -> renderResource (reversePackagesAll r) [format] + , reverseFlatUri = \format pkg -> renderResource (reversePackageFlat r) [display pkg, format] + , reverseVerboseUri = \format pkg -> renderResource (reversePackageVerbose r) [display pkg, format] } --textRevDisplay :: ReverseDisplay -> String --textRevDisplay m = unlines . map (\(n, (v, m)) -> display n ++ "-" ++ display v ++ ": " ++ show m) . Map.toList $ m --- If VersionStatus caching is used, revPackageId and revPackageName could be --- reduced to a single map lookup (see Distribution.Server.Packages.Reverse). -revPackageId :: MonadIO m => PackageId -> m ReverseDisplay -revPackageId pkgid = do - dispInfo <- revDisplayInfo - revs <- liftM reverseDependencies $ query GetReverseIndex - return $ perVersionReverse dispInfo revs pkgid - -revPackageName :: MonadIO m => PackageName -> m ReverseDisplay -revPackageName pkgname = do - dispInfo <- revDisplayInfo - revs <- liftM reverseDependencies $ query GetReverseIndex - return $ perPackageReverse dispInfo revs pkgname - -revDisplayInfo :: MonadIO m => m VersionIndex -revDisplayInfo = do - pkgIndex <- liftM packageList $ query GetPackagesState - prefs <- query GetPreferredVersions - return $ getDisplayInfo prefs pkgIndex - -data ReverseRender = ReverseRender { - rendRevPkg :: PackageId, - rendRevStatus :: Maybe VersionStatus, - rendRevCount :: Int -} deriving (Show, Eq, Ord) - -data ReversePageRender = ReversePageRender { - rendRevList :: [ReverseRender], - rendFilterCount :: (Int, Int), - rendPageTotal :: Int -} - -renderReverseWith :: MonadIO m => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender -renderReverseWith pkg rev filterFunc = do - counts <- liftM reverseCount $ query GetReverseIndex - let toRender (i, i') (pkgname, (version, status)) = case filterFunc status of - False -> (,) (i, i'+1) Nothing - True -> (,) (i+1, i') $ Just $ ReverseRender { - rendRevPkg = PackageIdentifier pkgname version, - rendRevStatus = status, - rendRevCount = maybe 0 directReverseCount $ Map.lookup pkgname counts - } - (res, rlist) = mapAccumL toRender (0, 0) (Map.toList rev) - pkgCount = maybe 0 directReverseCount $ Map.lookup pkg counts - return $ ReversePageRender (catMaybes rlist) res pkgCount - -renderReverseRecent :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender -renderReverseRecent pkg rev = renderReverseWith pkg rev $ \status -> case status of - Just DeprecatedVersion -> False - Nothing -> False - _ -> True - -renderReverseOld :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender -renderReverseOld pkg rev = renderReverseWith pkg rev $ \status -> case status of - Just DeprecatedVersion -> True - Nothing -> True - _ -> False - --- This could also differentiate between direct and indirect dependencies --- with a bit more calculation. -revPackageFlat :: MonadIO m => PackageName -> m [(PackageName, Int)] -revPackageFlat pkgname = do - index <- query GetReverseIndex - let counts = reverseCount index - count pkg = maybe 0 flattenedReverseCount $ Map.lookup pkg counts - pkgs = maybe [] Set.toList $ Map.lookup pkgname $ flattenedReverse index - return $ map (\pkg -> (pkg, count pkg)) pkgs - -revPackageStats :: MonadIO m => PackageName -> m ReverseCount -revPackageStats = query . GetReverseCount - -revPackageSummary :: MonadIO m => PackageId -> m (Int, Int) -revPackageSummary (PackageIdentifier pkgname version) = do - ReverseCount direct _ versions <- revPackageStats pkgname - return (direct, Map.findWithDefault 0 version versions) - --- This returns a list of (package name, direct dependencies, flat dependencies) --- for all packages. An interesting fact: it even does so for packages which --- don't exist in the index, except the latter two fields are always zero. This is --- because no versions of these packages exist, so the union of no versions is --- still no versions. TODO: use this fact to make an index of dependencies which --- are not in Hackage at all, which might be useful for fixing accidentally --- broken packages. -revSummary :: MonadIO m => m [(PackageName, Int, Int)] -revSummary = do - counts <- liftM reverseCount $ query GetReverseIndex - return $ map (\(pkg, ReverseCount direct flat _) -> (pkg, direct, flat)) $ Map.toList counts - -sortedRevSummary :: MonadIO m => ReverseFeature -> m [(PackageName, Int, Int)] -sortedRevSummary revs = Cache.getCache $ reverseTopCache revs - + -- If VersionStatus caching is used, revPackageId and revPackageName could be + -- reduced to a single map lookup (see Distribution.Server.Packages.Reverse). + queryReverseIndex :: MonadIO m => m ReverseIndex + queryReverseIndex = readMemState reverseMemState + + queryReverseDeps :: MonadIO m => PackageName -> m ([PackageName], [PackageName]) + queryReverseDeps pkgname = do + ms <- readMemState reverseMemState + let rdeps = getDependencies pkgname ms + rdepsall = getDependenciesFlat pkgname ms + indirect = Set.difference rdepsall rdeps + pure (Set.toList rdeps, Set.toList indirect) + + revPackageId :: MonadIO m => PackageId -> m ReverseDisplay + revPackageId pkgid = do + dispInfo <- revDisplayInfo + pkgIndex <- liftIO queryGetPackageIndex + revs <- queryReverseIndex + pure $ perVersionReverse dispInfo pkgIndex revs pkgid + + revPackageName :: MonadIO m => PackageName -> m ReverseDisplay + revPackageName pkgname = do + dispInfo <- revDisplayInfo + pkgIndex <- liftIO queryGetPackageIndex + revs <- queryReverseIndex + pure $ perPackageReverse dispInfo pkgIndex revs pkgname + + revJSON :: IO ByteString + revJSON = do + ReverseIndex revdeps nodemap _depmap <- queryReverseIndex + let assoc = takeWhile (\(a,_) -> a < Bimap.size nodemap) $ Arr.assocs . Gr.transposeG $ revdeps + nodeToString node = unPackageName (nodemap Bimap.!> node) + -- nodes = map (uncurry Node) $ map (\n -> (fst n, nodeToString (fst n))) assoc + edges = map (\(a,b) -> Edge a (nodeToString a) (map (\x-> nodeToString x) b)) assoc + return $ encode edges + + revDisplayInfo :: MonadIO m => m VersionIndex + revDisplayInfo = do + pkgIndex <- liftIO queryGetPackageIndex + prefs <- liftIO queryGetPreferredVersions + return $ getDisplayInfo prefs pkgIndex + + renderReverseWith :: MonadIO m => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender + renderReverseWith pkg rev filterFunc = do + let rev' = map fst $ Map.toList rev + directCounts <- mapM revDirectCount (pkg:rev') + let counts = zip (pkg:rev') directCounts + toRender (i, i') (pkgname, (version, status)) = if filterFunc status then (,) (i+1, i') $ Just ReverseRender { + rendRevPkg = PackageIdentifier pkgname version, + rendRevStatus = status, + rendRevCount = fromMaybe 0 $ lookup pkgname counts + } else (,) (i, i'+1) Nothing + (res, rlist) = mapAccumL toRender (0, 0) (Map.toList rev) + pkgCount = fromMaybe 0 $ lookup pkg counts + return $ ReversePageRender (catMaybes rlist) res pkgCount + + renderReverseRecent :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseRecent pkg rev = renderReverseWith pkg rev $ \status -> case status of + Just DeprecatedVersion -> False + Nothing -> False + _ -> True + + renderReverseOld :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseOld pkg rev = renderReverseWith pkg rev $ \status -> case status of + Just DeprecatedVersion -> True + Nothing -> True + _ -> False + + -- -- This could also differentiate between direct and indirect dependencies + -- -- with a bit more calculation. + revPackageFlat :: MonadIO m => PackageName -> m [(PackageName, Int)] + revPackageFlat pkgname = do + memState <- readMemState reverseMemState + let depList = Set.toList $ getDependenciesFlat pkgname memState + pure $ map (\d -> (d, getTotalCount d memState)) depList + + revPackageStats :: MonadIO m => PackageName -> m ReverseCount + revPackageStats pkgname = do + (direct, transitive) <- getReverseCount pkgname <$> readMemState reverseMemState + return $ ReverseCount direct transitive + + revDirectCount :: MonadIO m => PackageName -> m Int + revDirectCount pkgname = do + getDirectCount pkgname <$> readMemState reverseMemState + + -- This returns a list of (package name, direct dependencies, flat dependencies) + -- for all packages. An interesting fact: it even does so for packages which + -- don't exist in the index, except the latter two fields are always zero. This is + -- because no versions of these packages exist, so the union of no versions is + -- still no versions. TODO: use this fact to make an index of dependencies which + -- are not in Hackage at all, which might be useful for fixing accidentally + -- broken packages. + -- + -- The returned list is sorted ascendingly on directCount (see ReverseCount). + revCountForAllPackages :: MonadIO m => m [(PackageName, ReverseCount)] + revCountForAllPackages = do + index <- liftIO queryGetPackageIndex + let pkgnames = packageNames index + counts <- mapM revPackageStats pkgnames + return . sortOn (directCount . snd) $ zip pkgnames counts + + revForEachVersion :: PackageName -> IO (Map.Map Version (Set PackageIdentifier)) + revForEachVersion pkg = do + ReverseIndex revs nodemap depmap <- readMemState reverseMemState + index <- queryGetPackageIndex + let revDepNames :: [PackageName] + revDepNames = case Bimap.lookup pkg nodemap of + Nothing -> [] + Just nodeid -> mapMaybe (`Bimap.lookupR` nodemap) (Set.toList $ suc revs nodeid) + let -- The key is the version of 'pkg', and the values are specific + -- package versions that accept this version of pkg specified in the key + revDepVersions :: [(Version, Set PackageIdentifier)] + revDepVersions = do + x <- nubOrd revDepNames + pkginfo <- PackageIndex.lookupPackageName index pkg + pure (packageVersion pkginfo, dependsOnPkg index (packageId pkginfo) x depmap) + pure $ Map.fromListWith Set.union revDepVersions diff --git a/src/Distribution/Server/Features/ReverseDependencies/State.hs b/src/Distribution/Server/Features/ReverseDependencies/State.hs index 4a95db924..b66c65fdf 100644 --- a/src/Distribution/Server/Features/ReverseDependencies/State.hs +++ b/src/Distribution/Server/Features/ReverseDependencies/State.hs @@ -1,458 +1,233 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -module Distribution.Server.Features.ReverseDependencies.State where - -import Distribution.Server.Packages.Types -import Distribution.Server.Packages.State () -import Distribution.Server.Packages.Preferred -import Distribution.Server.Packages.PackageIndex (PackageIndex) -import qualified Distribution.Server.Packages.PackageIndex as PackageIndex +{-# LANGUAGE TupleSections #-} + +module Distribution.Server.Features.ReverseDependencies.State + ( ReverseIndex(..) + , ReverseDisplay + , ReverseCount(..) + , VersionIndex + , addPackage + , constructReverseIndex + , dependsOnPkg + , emptyReverseIndex + , getDepNames + , getDependencies + , getDependenciesFlat + , getDependenciesFlatRaw + , getDependenciesRaw + , getDirectCount + , getDisplayInfo + , getReverseCount + , getTotalCount + , perPackageReverse + , perVersionReverse + , suc + ) + where -import Distribution.Package -import Distribution.PackageDescription -import Distribution.Version +import Prelude hiding (lookup) -import Data.Acid (Query, Update, makeAcidic) -import Data.List (foldl', union) -import Data.Maybe (maybeToList, fromMaybe) -import Data.SafeCopy (base, deriveSafeCopy) -import Data.Typeable (Typeable) -import Data.Map (Map) +import Control.Arrow ((&&&)) +import qualified Data.Array as Arr ((!), assocs, accumArray) +import Data.Bimap (Bimap, lookup, lookupR) +import qualified Data.Bimap as Bimap +import Data.Containers.ListUtils (nubOrd) +import Data.List (union) +import Data.Map (Map) import qualified Data.Map as Map -import Data.Set (Set) +import Data.Maybe (mapMaybe, maybeToList) import qualified Data.Set as Set +import Data.Set (Set, fromList, toList, delete) +import Data.Typeable (Typeable) +import Data.Graph (Graph, Vertex) +import qualified Data.Graph as Gr + +import Distribution.Package +import Distribution.PackageDescription +import Distribution.Server.Packages.Types +import Distribution.Server.Framework.MemSize +import Distribution.Server.Features.PreferredVersions.State +import Distribution.Server.Packages.PackageIndex (PackageIndex) +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Version -import Data.STRef -import Control.Monad.ST -import Control.Monad.State (put, get) -import Control.Monad.Reader (ask, asks) - --- The main reverse dependencies map is a drawn-out Map PackageId PackageId, --- with an extra component to store ranges (all ranges that *could* be revdeps, --- even if no packages in the index currently satisfy those ranges). --- --- For selected entries of the map (foo, (2.0, (bar, [1.0]), (bar, (1.0, <3)))): --- This means that bar-1.0 depends on a version of foo <3, and foo 2.0 meets this criterion. -type RevDeps = Map PackageName (Map Version (Map PackageName (Set Version)), Map PackageName (Map Version VersionRange)) - -type CombinedDeps = Map PackageName VersionRange - --- TODO: should this be (Maybe (Version, Maybe VersionStatus))? --- it should be possible, albeit with a bit more work, to determine all revdeps --- of a package which don't have any versions presently satisfying them. --- (for an entry (a, b) of RevDeps, take (union a \ b)) -type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) +import Data.Binary (encode) +import qualified Data.ByteString.Lazy as BS -data ReverseIndex = ReverseIndex { - -- this field is a duplication of PackageIndex, so updates don't have - -- to use much information outside of this component, resulting in huge - -- happstack-state event files - duplicatedIndex :: PackageIndex PackageId, +emptyReverseIndex :: ReverseIndex +emptyReverseIndex = ReverseIndex (Gr.buildG (0,0) []) Bimap.empty mempty - -- The main reverse dependencies map - reverseDependencies :: RevDeps, +type NodeId = Int +type RevDeps = Graph - -- Generated from packageNameClosure. - flattenedReverse :: Map PackageName (Set PackageName), +data ReverseIndex = ReverseIndex + { reverseDependencies :: !RevDeps + , packageNodeIdMap :: !(Bimap PackageName NodeId) + , deps :: Map PackageIdentifier [Dependency] + } deriving (Eq, Show, Typeable) - -- Cached counts for reverse dependencies. - reverseCount :: Map PackageName ReverseCount -} deriving (Eq, Show, Typeable) +instance MemSize Dependency where + memSize = fromIntegral . BS.length . encode -emptyReverseIndex :: ReverseIndex -emptyReverseIndex = ReverseIndex (PackageIndex.fromList []) - Map.empty Map.empty Map.empty - -data ReverseCount = ReverseCount { - directReverseCount :: Int, - flattenedReverseCount :: Int, - versionReverseCount :: Map Version Int -} deriving (Show, Eq, Typeable, Ord) -emptyReverseCount :: ReverseCount -emptyReverseCount = ReverseCount 0 0 Map.empty +instance MemSize ReverseIndex where + memSize (ReverseIndex a b c) = memSize3 a b c constructReverseIndex :: PackageIndex PkgInfo -> ReverseIndex constructReverseIndex index = - let deps = constructRevDeps index - in updateReverseCount $ emptyReverseIndex { - duplicatedIndex = constructDupIndex index, - reverseDependencies = deps, - flattenedReverse = packageNameClosure deps - } - -constructDupIndex :: PackageIndex PkgInfo -> PackageIndex PackageId -constructDupIndex = PackageIndex.fromList - . map packageId - . PackageIndex.allPackages - -updateReverseCount :: ReverseIndex -> ReverseIndex -updateReverseCount index = - let deps = reverseDependencies index - flat = flattenedReverse index - in index { - reverseCount = flip Map.mapWithKey deps $ \pkg (versions, _) -> ReverseCount { - directReverseCount = Map.size . Map.unions $ Map.elems versions, - flattenedReverseCount = maybe 0 Set.size $ Map.lookup pkg flat, - versionReverseCount = Map.map Map.size versions + let nodePkgMap = foldr (uncurry Bimap.insert) Bimap.empty $ zip (PackageIndex.allPackageNames index) [0..] + (revs, dependencies) = constructRevDeps index nodePkgMap + in ReverseIndex + { reverseDependencies = revs + , packageNodeIdMap = nodePkgMap + , deps = dependencies } - } - -addPackage :: PackageId -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -addPackage pkgid deps revs = - let index = PackageIndex.insert pkgid (duplicatedIndex revs) - (rd, rec) = registerPackage (getAllVersions index) pkgid deps (reverseDependencies revs) - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd, - flattenedReverse = packageNameClosure rd - }, rec) - -removePackage :: PackageId -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -removePackage pkgid deps revs = - let index = PackageIndex.deletePackageId pkgid (duplicatedIndex revs) - (rd, rec) = unregisterPackage (getAllVersions index) pkgid deps (reverseDependencies revs) - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd, - flattenedReverse = packageNameClosure rd - }, rec) - -changePackage :: PackageId -> CombinedDeps -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -changePackage pkgid deps deps' revs = - let index = PackageIndex.insert pkgid (duplicatedIndex revs) - allVersions = getAllVersions index - (rd, rec) = unregisterPackage allVersions pkgid deps (reverseDependencies revs) - (rd', rec') = registerPackage allVersions pkgid deps' rd - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd', - flattenedReverse = packageNameClosure rd' - }, Map.unionWith union rec rec') --------------------------------------------------------------------------------- --- Managing the RevDeps index. - -constructRevDeps :: PackageIndex PkgInfo -> RevDeps -constructRevDeps index = foldl' (\revs pkg -> fst $ registerPackage (getAllVersions index) (packageId pkg) (getAllDependencies pkg) revs) Map.empty $ PackageIndex.allPackages index - -getAllVersions :: Package pkg => PackageIndex pkg -> PackageName -> [Version] -getAllVersions index = map packageVersion . PackageIndex.lookupPackageName index - --- | Given a package id, modify the entries of the package's dependencies in --- the reverse dependencies mapping to include it. -registerPackage :: (PackageName -> [Version]) - -> PackageId -> CombinedDeps - -> RevDeps -> (RevDeps, Map PackageName [Version]) -registerPackage getVersions (PackageIdentifier name version) ranges revs = - let deps = getLinkedNodes getVersions ranges - revs' = foldl' goRegister revs $ Map.toList $ Map.intersectionWith (,) ranges deps - revs'' = backtrace revs' - in (revs'', deps) - where - pkgMap = Map.singleton name $ Set.singleton version - -- this takes each of the registered packages dependencies and puts an entry - -- of it there. e.g. a new version of base would not have much work to do - -- here because it has no dependencies - goRegister prev (pkgname, (range, versions)) = - -- revPackage encodes the dependency (name -> pkgname) in a way that can - -- be inserted into pkgname's reverse dependency mapping - let revPackage = Map.fromList $ map (\v -> (v, pkgMap)) versions - revRange = Map.singleton name (Map.singleton version range) - in Map.insertWith (\(small, small') (big, big') -> (Map.unionWith (Map.unionWith Set.union) big small, Map.unionWith (flip const) big' small')) pkgname (revPackage, revRange) prev - -- this uses the package's existing reverse dependencies, regardless of - -- version, to find reverse dependencies for this version in particular. - -- e.g. a new version of base would have to recalculate the dependencies - -- of nearly all of the packages in the index - backtrace prev = case Map.lookup name prev of - Nothing -> prev - Just (vs, rs) -> - let revVersion = Map.map (Set.fromList . map fst . filter (withinRange version . snd) . Map.toList) rs - in Map.insert name (Map.insertWith (\new old -> Map.unionWith Set.union old new) version revVersion vs, rs) prev - --- | Given a package id, modify the entries of the package's dependencies in --- the reverse dependencies mapping to exclude it. -unregisterPackage :: (PackageName -> [Version]) - -> PackageId -> CombinedDeps - -> RevDeps -> (RevDeps, Map PackageName [Version]) -unregisterPackage getVersions (PackageIdentifier name version) ranges revs = - let deps = getLinkedNodes getVersions ranges - revs' = foldl' goUnregister revs $ Map.toList $ Map.intersectionWith (,) ranges deps - revs'' = backtrace revs' - in (revs'', deps) - where - pkgMap = Map.singleton name $ Set.singleton version - goUnregister prev (pkgname, (range, versions)) = - let revPackage = Map.fromList $ map (\v -> (v, pkgMap)) versions - revRange = Map.singleton pkgname (Map.fromList $ map (\v -> (v, range)) versions) - -- there are possibly better ways to go about this - in Map.differenceWith (\(a, b) (c, d) -> keepMaps - ( Map.differenceWith (\e f -> - keepMap $ Map.differenceWith (\g h -> - keepSet $ Set.difference g h) - e f) - a c - , Map.differenceWith (\e f -> - keepMap $ Map.difference e f) - b d - )) prev (Map.singleton pkgname (revPackage, revRange)) - backtrace prev = Map.update (\(vs, rs) -> keepMaps (Map.delete version vs, rs)) name prev --------------------------------------------------------------------------------- --- Calculating dependencies and selecting versions - --- | Given a package, determine the packages on which it depends. --- For all such packages, return the specific versions that satisfy the --- dependency as indicated in the cabal file. -getLinkedNodes :: (PackageName -> [Version]) - -> CombinedDeps -> Map PackageName [Version] -getLinkedNodes getVersions pkgs = Map.mapWithKey (\pkg range -> selectVersions range $ getVersions pkg) pkgs - --- | Given a dependency (a package name and a version range), find all versions --- in the current package index that satisfy it. -selectVersions :: VersionRange -> [Version] -> [Version] -selectVersions range versions= filter (flip withinRange range) versions - --- | Collect all dependencies specified in a package's cabal file, considering --- all alternatives. --- --- This unions all version ranges together from different branches, which is --- imprecise but mostly good enough (a slightly better heuristic might be --- intersecting within a block, but unioning blocks together). -getAllDependencies :: PkgInfo -> CombinedDeps -getAllDependencies pkg = - let desc = pkgDesc pkg - in Map.fromListWith unionVersionRanges $ toDepsList (maybeToList $ condLibrary desc) - ++ toDepsList (map snd $ condExecutables desc) - where toDepsList :: [CondTree v [Dependency] a] -> [(PackageName, VersionRange)] - toDepsList l = [ (p, v) | Dependency p v <- concatMap harvestDependencies l ] +addPackage :: PackageIndex PkgInfo -> PackageName -> [PackageName] + -> ReverseIndex -> ReverseIndex +addPackage index pkgname dependencies (ReverseIndex revs nodemap pkgIdToDeps) = + let npm = Bimap.tryInsert pkgname (Bimap.size nodemap) nodemap + pn = (:[]) <$> lookup pkgname npm + new :: [(Int, [Int])] + new = mapMaybe (\d -> (,) <$> lookup d npm <*> pn) dependencies + rd = insEdges (Bimap.size npm) new revs + pkginfos = PackageIndex.lookupPackageName index pkgname + newPackageDepMap = Map.fromList $ map (packageId &&& getDeps) pkginfos + in ReverseIndex + { reverseDependencies = rd + , packageNodeIdMap = npm + , deps = Map.union newPackageDepMap pkgIdToDeps + } + +constructRevDeps :: PackageIndex PkgInfo -> Bimap PackageName NodeId -> (RevDeps, Map PackageIdentifier [Dependency]) +constructRevDeps index nodemap = + let allPackages :: [PkgInfo] + allPackages = concat $ PackageIndex.allPackagesByName index + nodeIdsOfDependencies :: PkgInfo -> [(NodeId, NodeId)] + nodeIdsOfDependencies pkg = mapMaybe (\dep -> (,) <$> lookup dep nodemap <*> lookup (packageName pkg) nodemap) (getDepNames pkg) + -- This will mix dependencies of different versions of the same package, but that is intended. + edges = map nodeIdsOfDependencies allPackages + dependencies = Map.fromList $ map (packageId &&& getDeps) allPackages + + in (Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) + , dependencies + ) + +getDeps :: PkgInfo -> [Dependency] +getDeps pkg = + concatMap harvestDependencies (maybeToList $ condLibrary $ pkgDesc pkg) + +getDepNames :: PkgInfo -> [PackageName] +getDepNames pkg = + map depPkgName $ getDeps pkg + +-- | Returns [containers 0.5.0.0 to 0.6.0.1] for needle=ghc-prim-0.6.0.0 and packageHaystack=containers +-- | because these are the versions that could accept that version of ghc-prim as a dep +-- | Note that this doesn't include executables! Only the library. +dependsOnPkg :: PackageIndex PkgInfo -> PackageId -> PackageName -> Map PackageIdentifier [Dependency] -> Set PackageIdentifier +dependsOnPkg index needle packageHaystack dependencies = + fromList $ mapMaybe descPermits (PackageIndex.lookupPackageName index packageHaystack) + where + descPermits :: PkgInfo -> Maybe PackageIdentifier + descPermits pkginfo + | Just found <- Map.lookup (packageId pkginfo) dependencies + , any toDepsList found = Just (packageId pkginfo) + | otherwise = Nothing + toDepsList (Dependency name versionRange _) = + packageVersion needle `withinRange` versionRange + && packageName needle == name -- | Collect all dependencies from all branches of a condition tree. harvestDependencies :: CondTree v [Dependency] a -> [Dependency] -harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps - where forComponent (_, iftree, elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree +harvestDependencies (CondNode _ dependencies comps) = dependencies ++ concatMap forComponent comps + where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree + +---------------------------------ReverseDisplay -------------------------------------------------------------------------------- -- Calculating ReverseDisplays -type VersionIndex = (PackageName -> (PreferredInfo, [Version])) +data ReverseCount = ReverseCount + { directCount :: Int + , totalCount :: Int + } deriving (Show, Eq, Typeable, Ord) --- TODO: this should use the secondary PackageId -> VersionRange mapping in RevDeps, --- so it gets all possible versions, not just those currently in the index. -perPackageReverse :: VersionIndex -> RevDeps -> PackageName -> ReverseDisplay -perPackageReverse indexFunc revs pkg = case Map.lookup pkg revs of - Nothing -> Map.empty - Just (dict, _) -> constructReverseDisplay indexFunc (Map.unionsWith Set.union $ Map.elems dict) +instance MemSize ReverseCount where + memSize (ReverseCount a b) = memSize2 a b -perVersionReverse :: VersionIndex -> RevDeps -> PackageId -> ReverseDisplay -perVersionReverse indexFunc revs pkg = case Map.lookup (packageVersion pkg) . fst =<< Map.lookup (packageName pkg) revs of - Nothing -> Map.empty - Just dict -> constructReverseDisplay indexFunc dict +type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) + +type VersionIndex = (PackageName -> (PreferredInfo, [Version])) -constructReverseDisplay :: VersionIndex -> Map PackageName (Set Version) -> ReverseDisplay -constructReverseDisplay indexFunc deps = - Map.mapMaybeWithKey (uncurry maybeBestVersion . indexFunc) deps +perPackageReverse :: (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageName -> Map PackageName (Version, Maybe VersionStatus) +perPackageReverse indexFunc index revdeps pkg = + let pkgids = (packageVersion. packageId) <$> PackageIndex.lookupPackageName index pkg + best :: PackageId + best = PackageIdentifier pkg (maximum pkgids) + in perVersionReverse indexFunc index revdeps best + +perVersionReverse :: (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> Map PackageName (Version, Maybe VersionStatus) +perVersionReverse indexFunc index (ReverseIndex revs nodemap dependencies) pkg = case lookup (packageName pkg) nodemap of + Nothing -> Map.empty + Just found -> + -- this will be too much, since we are throwing away the specific version + let revDepNames = mapMaybe (`lookupR` nodemap) (toList $ suc revs found) + packagemap :: Map PackageName (Set Version) + packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x dependencies)) revDepNames + in constructReverseDisplay indexFunc packagemap + +constructReverseDisplay :: (PackageName -> (PreferredInfo, [Version])) -> Map PackageName (Set Version) -> Map PackageName (Version, Maybe VersionStatus) +constructReverseDisplay indexFunc = + Map.mapMaybeWithKey (uncurry maybeBestVersion . indexFunc) getDisplayInfo :: PreferredVersions -> PackageIndex PkgInfo -> VersionIndex getDisplayInfo preferred index pkgname = (,) (Map.findWithDefault emptyPreferredInfo pkgname $ preferredMap preferred) (map packageVersion . PackageIndex.lookupPackageName index $ pkgname) --------------------------------------------------------------------------------- --- Keeping a cached map of selected versions. --- --- Currently it's rather quick to calculate each, and the majority of processing --- will probably go towards rendering it in HTML/JSON/whathaveyou anyway. So this --- is not used. --- --- Still, in a future Hackage where preferred-versions and deprecated versions --- are *very* widely used, incremental updates of an display index might become --- necessary. In such a future, there would be two maps in the ReverseIndex --- structure, both to ReverseDisplay from PackageName and PackageId. They would --- need to be updated with updatePackageReverse and updateVersionReverse, --- respectively, whenever a package is added, removed, or has its preferred info --- changed. - -constructPackageReverse :: VersionIndex -> RevDeps -> Map PackageName ReverseDisplay -constructPackageReverse indexFunc revs = - Map.fromList $ do - pkg <- Map.keys revs - rev <- maybeToList . keepMap $ perPackageReverse indexFunc revs pkg - return (pkg, rev) - -constructVersionReverse :: VersionIndex -> RevDeps -> Map PackageId ReverseDisplay -constructVersionReverse indexFunc revs = - Map.fromList $ do - pkg <- getNodes =<< Map.toList revs - rev <- maybeToList . keepMap $ perVersionReverse indexFunc revs pkg - return (pkg, rev) - where - getNodes :: (PackageName, (Map Version a, b)) -> [PackageId] - getNodes (name, (versions, _)) = map (PackageIdentifier name) $ Map.keys versions - --- | With a package which has just been updated, make sure the version displayed --- in its reverse display is the most recent. To do this, each of its dependencies --- needs its ReverseDisplay updated. -updateReverseDisplay :: VersionIndex -> PackageName -> Set Version -> ReverseDisplay -> ReverseDisplay -updateReverseDisplay indexFunc pkgname versions revDisplay = - let toVersions = uncurry maybeBestVersion . indexFunc - in case toVersions pkgname versions of - Nothing -> revDisplay - Just status -> Map.insert pkgname status revDisplay - --- If the RevDeps index is modified through registering/unregistering packages, --- updatePackageReverse and updateVersionReverse should be given a list of --- package names/package ids distrilled from the resultant (Map PackageName --- [Version]). The idea is to sync it with the just-updated RevDeps. --- --- If a package's PreferredVersions are modified, these functions should be --- called with the same information taken from the getLinkedNodes function. --- In this case, the RevDeps data structure hasn't changed. - -updatePackageReverse :: VersionIndex -> PackageName -> [PackageName] -> RevDeps -> Map PackageName ReverseDisplay -> Map PackageName ReverseDisplay -updatePackageReverse indexFunc updated deps revs nameMap = - foldl' (\revd pkg -> Map.alter (alterRevDisplay pkg . fromMaybe Map.empty) pkg revd) nameMap deps - where - lookupVersions :: PackageName -> Set Version - lookupVersions pkgname = maybe Set.empty (Set.unions . map (Map.findWithDefault Set.empty updated) . Map.elems . fst) $ Map.lookup pkgname revs - alterRevDisplay :: PackageName -> ReverseDisplay -> Maybe ReverseDisplay - alterRevDisplay pkgname rev = keepMap $ updateReverseDisplay indexFunc updated (lookupVersions pkgname) rev - -updateVersionReverse :: VersionIndex -> PackageName -> [PackageId] -> RevDeps -> Map PackageId ReverseDisplay -> Map PackageId ReverseDisplay -updateVersionReverse indexFunc updated deps revs pkgMap = - foldl' (\revd pkg -> Map.alter (alterRevDisplay pkg . fromMaybe Map.empty) pkg revd) pkgMap deps - where - lookupVersions :: PackageId -> Set Version - lookupVersions pkgid = maybe Set.empty (Map.findWithDefault Set.empty updated) $ Map.lookup (packageVersion pkgid) . fst =<< Map.lookup (packageName pkgid) revs - alterRevDisplay :: PackageId -> ReverseDisplay -> Maybe ReverseDisplay - alterRevDisplay pkgid rev = keepMap $ updateReverseDisplay indexFunc updated (lookupVersions pkgid) rev - --------------------------------------------------------------------------------- --- Flattening the graph --- Exposing indirect dependencies is as simple as taking the set difference --- of the edges of a node in the dependency graph G and its closure G+. - --- Collect all indirect versioned dependencies. This takes around 45 seconds --- on the current package index (in ghci). It probably isn't worth exposing. -packageIdClosure :: RevDeps -> Map PackageId (Set PackageId) -packageIdClosure revs = Map.fromDistinctAscList $ transitiveClosure - (concatMap getNodes $ Map.toList revs) - (\pkg -> maybe [] (concatMap getEdges . Map.toList) - $ Map.lookup (packageVersion pkg) . fst =<< Map.lookup (packageName pkg) revs) - where - getNodes :: (PackageName, (Map Version a, b)) -> [PackageId] - getNodes (name, (versions, _)) = map (PackageIdentifier name) $ Map.keys versions - - getEdges :: (PackageName, Set Version) -> [PackageId] - getEdges (name, versions) = map (PackageIdentifier name) $ Set.toList versions - --- Collect all indirect name dependencies. This takes around 2 seconds on the --- current package index. It should be fine to reconstruct from scratch every --- time the package index is updated, since code to incrementally update a --- transitive closure can be messy and stateful and complicated. -packageNameClosure :: RevDeps -> Map PackageName (Set PackageName) -packageNameClosure revs = Map.fromDistinctAscList $ transitiveClosure - (Map.keys revs) - (\pkg -> maybe [] (concatMap Map.keys . Map.elems . fst) - $ Map.lookup pkg revs) - --- Get the transitive closure of a graph from the set of nodes and a neighbor --- function. This implementation uses depth-first search in the ST monad --- where cycles are broken if a visited node has been seen before. --- --- The same basic algorithm could be used to make a DAG structure. -transitiveClosure :: forall a. Ord a => [a] -> (a -> [a]) -> [(a, Set a)] -transitiveClosure core edges = runST $ do - list <- mapM (\node -> newSTRef Nothing >>= \ref -> return (node, ref)) core - let visited = Map.fromList list - mapM_ (collect visited) core - list' <- mapM (\(node, ref) -> readSTRef ref >>= \val -> return (node, val)) list - return [ (node, nodes) | (node, Just nodes) <- list' ] - where - collect :: Map a (STRef s (Maybe (Set a))) -> a -> ST s (Set a) - collect links node = do - case Map.lookup node links of - -- attempting to visit a node which wasn't given to us - Nothing -> return Set.empty - Just ref -> readSTRef ref >>= \t -> case t of - -- the node has already been visited - Just calc -> return calc - Nothing -> do - -- Mark the node as visited with results pending. If a cycle - -- brings us back to collect the same node, it will yield - -- an empty list. This breaks the cycle for whatever node - -- was visited first. - -- - -- There are smarter algorithms that can - -- get transitive closures of cyclic graphs, but cycles are - -- highly pathological for the types of graphs we'll be - -- traversing, so no need to worry. - writeSTRef ref $ Just Set.empty - let outEdges = edges node - collected <- fmap Set.unions $ mapM (collect links) outEdges - let connected = Set.union (Set.fromList outEdges) collected - writeSTRef ref $ Just connected - return connected ------------------------------------ Utility --- For cases when, if a Map or Set is empty, it's as good as nothing at all. -keepMap :: Ord k => Map k a -> Maybe (Map k a) -keepMap con = if Map.null con then Nothing else Just con -keepMaps :: (Ord k, Ord k') => (Map k a, Map k' b) -> Maybe (Map k a, Map k' b) -keepMaps con@(c, c') = if Map.null c && Map.null c' then Nothing else Just con - -keepSet :: Ord a => Set a -> Maybe (Set a) -keepSet con = if Set.null con then Nothing else Just con - --------------------------------------------------------------------------------- --- State --- --- Last but agnostic of other ranking schemes, --- methods for manipulating the global state. +----------------------------Graph Utility---------- +suc :: RevDeps -> Vertex -> Set Vertex +suc g v = fromList $ g Arr.! v -deriveSafeCopy 0 'base ''ReverseIndex -deriveSafeCopy 0 'base ''ReverseCount +insEdges :: Int -> [(NodeId, [NodeId])] -> RevDeps -> RevDeps +insEdges nodesize edges revdeps = Arr.accumArray union [] (0, nodesize) (edges ++ Arr.assocs revdeps) -initialReverseIndex :: ReverseIndex -initialReverseIndex = emptyReverseIndex +-------------------------------------- -getReverseIndex :: Query ReverseIndex ReverseIndex -getReverseIndex = ask +getDependencies :: PackageName -> ReverseIndex -> Set PackageName +getDependencies pkg revs = names revs $ getDependenciesRaw pkg revs -replaceReverseIndex :: ReverseIndex -> Update ReverseIndex () -replaceReverseIndex = put +getDependenciesRaw :: PackageName -> ReverseIndex -> Set NodeId +getDependenciesRaw pkg (ReverseIndex revdeps nodemap _) = + case lookup pkg nodemap of + Nothing -> mempty + Just nodeid -> delete nodeid (suc revdeps nodeid) -addReversePackage :: PackageId -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -addReversePackage pkgid deps = get >>= \revs -> - let (revs', rec) = addPackage pkgid deps revs - in put revs' >> return rec +-- | The flat/total/transitive/indirect reverse dependencies are all the packages that depend on something that depends on the given 'pkg' +getDependenciesFlat :: PackageName -> ReverseIndex -> Set PackageName +getDependenciesFlat pkg revs = names revs $ getDependenciesFlatRaw pkg revs -removeReversePackage :: PackageId -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -removeReversePackage pkgid deps = get >>= \revs -> - let (revs', rec) = removePackage pkgid deps revs - in put revs' >> return rec +getDependenciesFlatRaw :: PackageName -> ReverseIndex -> Set NodeId +getDependenciesFlatRaw pkg (ReverseIndex revdeps nodemap _) = do + case lookup pkg nodemap of + Nothing -> mempty + Just nodeid -> delete nodeid $ fromList (Gr.reachable revdeps nodeid) -changeReversePackage :: PackageId -> CombinedDeps -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -changeReversePackage pkgid deps deps' = get >>= \revs -> - let (revs', rec) = changePackage pkgid deps deps' revs - in put revs' >> return rec +-- | The direct dependencies depend on the given 'pkg' directly, i.e. not transitively +getDirectCount :: PackageName -> ReverseIndex -> Int +getDirectCount pkg revs = length $ getDependenciesRaw pkg revs -getReverseCount :: PackageName -> Query ReverseIndex ReverseCount -getReverseCount pkg = asks $ Map.findWithDefault emptyReverseCount pkg . reverseCount +-- | Given a set of NodeIds, look up the package names for all of them +names :: ReverseIndex -> Set NodeId -> Set PackageName +names (ReverseIndex _ nodemap _) ids = do + fromList $ mapMaybe (`lookupR` nodemap) (toList ids) -getFlattenedReverse :: PackageName -> Query ReverseIndex (Set PackageName) -getFlattenedReverse pkg = asks $ Map.findWithDefault Set.empty pkg . flattenedReverse -makeAcidic ''ReverseIndex ['getReverseIndex - ,'replaceReverseIndex - ,'addReversePackage - ,'removeReversePackage - ,'changeReversePackage - ,'getReverseCount - ,'getFlattenedReverse - ] +-- | The flat/total/transitive/indirect dependency count is the amount of package names that depend transitively on the given 'pkg' +getTotalCount :: PackageName -> ReverseIndex -> Int +getTotalCount pkg revs = length $ getDependenciesFlatRaw pkg revs +getReverseCount :: PackageName -> ReverseIndex -> (Int, Int) +getReverseCount pkg revs = (getDirectCount pkg revs, getTotalCount pkg revs) diff --git a/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs b/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs index 24a8334df..eab6563dc 100644 --- a/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs +++ b/src/Distribution/Server/Features/Search/ExtractDescriptionTerms.hs @@ -12,7 +12,7 @@ import Data.Set (Set) import qualified Data.Set as Set import Data.Char import qualified NLP.Tokenize as NLP -import qualified NLP.Snowball as NLP +import qualified Distribution.Server.Util.NLP.Snowball as NLP import qualified Data.Foldable as F import qualified Documentation.Haddock.Markup as Haddock @@ -26,7 +26,7 @@ extraStems ss x = x : mapMaybe (`T.stripSuffix` x) ss extractSynopsisTerms :: [Text] -> Set Text -> String -> [Text] extractSynopsisTerms ss stopWords = concatMap (extraStems ss) --note this adds extra possible stems, it doesn't delete any given one. - . NLP.stems NLP.English + . NLP.stems . filter (`Set.notMember` stopWords) . map (T.toCaseFold . T.pack) . concatMap splitTok @@ -54,7 +54,7 @@ splitTok tok = extractDescriptionTerms :: [Text] -> Set Text -> String -> [Text] extractDescriptionTerms ss stopWords = concatMap (extraStems ss) - . NLP.stems NLP.English + . NLP.stems . filter (`Set.notMember` stopWords) . map (T.toCaseFold . T.pack) . maybe @@ -78,7 +78,7 @@ termsMarkup = Markup { markupBold = id, markupMonospaced = \s -> if length s > 1 then [] else s, markupUnorderedList = concat, - markupOrderedList = concat, + markupOrderedList = concat . map snd, markupDefList = concatMap (\(d,t) -> d ++ t), markupCodeBlock = const [], markupTable = concat . F.toList, diff --git a/src/Distribution/Server/Features/Search/PkgSearch.hs b/src/Distribution/Server/Features/Search/PkgSearch.hs index 9465986d9..2f8078b86 100644 --- a/src/Distribution/Server/Features/Search/PkgSearch.hs +++ b/src/Distribution/Server/Features/Search/PkgSearch.hs @@ -14,7 +14,7 @@ import Data.Set (Set) import qualified Data.Set as Set import Data.Text (Text, unpack) import qualified Data.Text as T -import NLP.Snowball +import Distribution.Server.Util.NLP.Snowball import Distribution.Package import Distribution.PackageDescription @@ -62,7 +62,7 @@ pkgSearchConfig = normaliseQueryToken tok = let tokFold = T.toCaseFold tok -- we don't need to use extraStems here because the index is inflated by it already. - tokStem = stem English tokFold + tokStem = stem tokFold in \field -> case field of NameField -> tokFold SynopsisField -> tokStem diff --git a/src/Distribution/Server/Features/Security/Backup.hs b/src/Distribution/Server/Features/Security/Backup.hs index 5911d488f..a0afe21b1 100644 --- a/src/Distribution/Server/Features/Security/Backup.hs +++ b/src/Distribution/Server/Features/Security/Backup.hs @@ -6,7 +6,7 @@ module Distribution.Server.Features.Security.Backup ( ) where -- stdlib -import Control.Monad.State +import Control.Monad.State (StateT, execStateT, modify) import Data.Time import Data.Version (Version(..), showVersion) import Text.CSV hiding (csv) @@ -224,7 +224,7 @@ import_v1 = mapM_ fromRecord fromInfoRecord [strFileLength, strSHA256, strMD5] = do fileInfoLength <- parseRead "file length" strFileLength fileInfoSHA256 <- parseSHA "file SHA256" strSHA256 - fileInfoMD5 <- Just `liftM` parseMD5 "file MD5" strMD5 + fileInfoMD5 <- Just <$> parseMD5 "file MD5" strMD5 return FileInfo{..} fromInfoRecord otherRecord = fail $ "Unexpected info record: " ++ show otherRecord diff --git a/src/Distribution/Server/Features/Security/Migration.hs b/src/Distribution/Server/Features/Security/Migration.hs index 5fb09f054..b9ee61f50 100644 --- a/src/Distribution/Server/Features/Security/Migration.hs +++ b/src/Distribution/Server/Features/Security/Migration.hs @@ -206,11 +206,11 @@ data Migrated a = Migrated MigrationStats a | AlreadyMigrated a deriving (Functor) instance Applicative Migrated where - pure = return + pure = AlreadyMigrated f <*> x = do f' <- f ; x' <- x ; return $ f' x' instance Monad Migrated where - return = AlreadyMigrated + return = pure AlreadyMigrated a >>= f = f a Migrated stats a >>= f = case f a of diff --git a/src/Distribution/Server/Features/Security/State.hs b/src/Distribution/Server/Features/Security/State.hs index a56b417b5..dad42fcd8 100644 --- a/src/Distribution/Server/Features/Security/State.hs +++ b/src/Distribution/Server/Features/Security/State.hs @@ -7,7 +7,8 @@ module Distribution.Server.Features.Security.State where -- stdlib -import Control.Monad.Reader +import Control.Monad +import Control.Monad.Reader (ask, asks) import Data.Acid import Data.Maybe import Data.SafeCopy diff --git a/src/Distribution/Server/Features/Sitemap.hs b/src/Distribution/Server/Features/Sitemap.hs index 198452c39..9227d70e7 100644 --- a/src/Distribution/Server/Features/Sitemap.hs +++ b/src/Distribution/Server/Features/Sitemap.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards, NamedFieldPuns, RecursiveDo #-} +{-# LANGUAGE TupleSections #-} module Distribution.Server.Features.Sitemap ( SitemapFeature(..) @@ -25,7 +26,25 @@ import Data.ByteString.Lazy (ByteString) import Data.Time.Clock (UTCTime(..), getCurrentTime) import Data.Time.Calendar (showGregorian) import Network.URI +import Control.DeepSeq +import Text.Read +import Data.List.Split +import Distribution.Server.Framework.BlobStorage +import Distribution.Server.Features.TarIndexCache +import qualified Data.TarIndex as Tar +import System.FilePath (takeExtension) +data Sitemap + = Sitemap + { sitemapIndex :: XMLResponse + , sitemaps :: [XMLResponse] + } + +instance NFData Sitemap where + rnf (Sitemap i s) = rnf i `seq` rnf s + +instance MemSize Sitemap where + memSize (Sitemap i s) = memSize2 i s data SitemapFeature = SitemapFeature { sitemapFeatureInterface :: HackageFeature @@ -38,6 +57,7 @@ initSitemapFeature :: ServerEnv -> IO ( CoreFeature -> DocumentationFeature -> TagsFeature + -> TarIndexCacheFeature -> IO SitemapFeature) initSitemapFeature env@ServerEnv{ serverCacheDelay, @@ -46,10 +66,11 @@ initSitemapFeature env@ServerEnv{ serverCacheDelay, return $ \coref@CoreFeature{..} docsCore@DocumentationFeature{..} - tagsf@TagsFeature{..} -> do + tagsf@TagsFeature{..} + tarf@TarIndexCacheFeature{..} -> do rec let (feature, updateSitemapCache) = - sitemapFeature env coref docsCore tagsf + sitemapFeature env coref docsCore tagsf tarf initTime sitemapCache sitemapCache <- newAsyncCacheNF updateSitemapCache @@ -66,63 +87,85 @@ sitemapFeature :: ServerEnv -> CoreFeature -> DocumentationFeature -> TagsFeature + -> TarIndexCacheFeature -> UTCTime - -> AsyncCache XMLResponse - -> (SitemapFeature, IO XMLResponse) + -> AsyncCache Sitemap + -> (SitemapFeature, IO Sitemap) sitemapFeature ServerEnv{..} CoreFeature{..} DocumentationFeature{..} TagsFeature{..} + TarIndexCacheFeature{cachedTarIndex} initTime sitemapCache = (SitemapFeature{..}, updateSitemapCache) where sitemapFeatureInterface = (emptyHackageFeature "sitemap") { - featureResources = [ xmlSitemapResource ] + featureResources = [ xmlSitemapIndexResource, xmlSitemapResource ] , featureState = [] - , featureDesc = "Provides a sitemap.xml for search engines" + , featureDesc = "Provides sitemap for search engines" , featureCaches = [ CacheComponent { - cacheDesc = "sitemap.xml", + cacheDesc = "sitemap", getCacheMemSize = memSize <$> readAsyncCache sitemapCache } ] , featurePostInit = do syncAsyncCache sitemapCache addCronJob serverCron CronJob { - cronJobName = "regenerate the cached sitemap.xml", + cronJobName = "regenerate the cached sitemap", cronJobFrequency = DailyJobFrequency, cronJobOneShot = False, cronJobAction = prodAsyncCache sitemapCache "cron" } } + xmlSitemapIndexResource :: Resource + xmlSitemapIndexResource = (resourceAt "/sitemap_index.xml") { + resourceDesc = [(GET, "The dynamically generated sitemap index, in XML format")] + , resourceGet = [("xml", serveSitemapIndex)] + } + xmlSitemapResource :: Resource - xmlSitemapResource = (resourceAt "/sitemap.xml") { + xmlSitemapResource = (resourceAt "/sitemap/:filename") { resourceDesc = [(GET, "The dynamically generated sitemap, in XML format")] , resourceGet = [("xml", serveSitemap)] } - serveSitemap :: DynamicPath -> ServerPartE Response - serveSitemap _ = do - sitemapXML <- liftIO $ readAsyncCache sitemapCache + serveSitemapIndex :: DynamicPath -> ServerPartE Response + serveSitemapIndex _ = do + Sitemap{..} <- liftIO $ readAsyncCache sitemapCache cacheControlWithoutETag [Public, maxAgeDays 1] - return (toResponse sitemapXML) + return (toResponse sitemapIndex) + + serveSitemap :: DynamicPath -> ServerPartE Response + serveSitemap dpath = + case lookup "filename" dpath of + Just filename + | [basename, "xml"] <- splitOn "." filename + , Just i <- readMaybe basename -> do + Sitemap{..} <- liftIO $ readAsyncCache sitemapCache + guard (i < length sitemaps) + cacheControlWithoutETag [Public, maxAgeDays 1] + return (toResponse (sitemaps !! i)) + _ -> mzero -- Generates a list of sitemap entries corresponding to hackage pages, then -- builds and returns an XML sitemap. - updateSitemapCache :: IO XMLResponse + updateSitemapCache :: IO Sitemap updateSitemapCache = do alltags <- queryGetTagList pkgIndex <- queryGetPackageIndex docIndex <- queryDocumentationIndex - let sitemap = generateSitemap serverBaseURI pageBuildDate + sitemaps <- generateSitemap serverBaseURI pageBuildDate (map fst alltags) - pkgIndex docIndex - return (XMLResponse sitemap) + pkgIndex docIndex cachedTarIndex + let uriScheme i = "/sitemap/" <> show i <> ".xml" + sitemapIndex = renderSitemapIndex serverBaseURI (map uriScheme [0..(length sitemaps - 1)]) + return $ Sitemap (XMLResponse sitemapIndex) (map XMLResponse sitemaps) pageBuildDate :: T.Text pageBuildDate = T.pack (showGregorian (utctDay initTime)) @@ -131,19 +174,21 @@ generateSitemap :: URI -> T.Text -> [Tag] -> PackageIndex.PackageIndex PkgInfo - -> Map.Map PackageId a - -> ByteString -generateSitemap serverBaseURI pageBuildDate alltags pkgIndex docIndex = - renderSitemap serverBaseURI allEntries + -> Map.Map PackageId BlobId + -> (BlobId -> IO Tar.TarIndex) + -> IO [ByteString] +generateSitemap serverBaseURI pageBuildDate alltags pkgIndex docIndex cachedTarIndex = do + versionedDocSubEntries <- versionedDocSubEntriesIO + let -- Combine and build sitemap + allEntries = miscEntries + ++ tagEntries + ++ nameEntries + ++ nameVersEntries + ++ baseDocEntries + ++ versionedDocEntries + ++ versionedDocSubEntries + pure $ renderSitemap serverBaseURI <$> chunksOf 50000 allEntries where - -- Combine and build sitemap - allEntries = miscEntries - ++ tagEntries - ++ nameEntries - ++ nameVersEntries - ++ baseDocEntries - ++ versionedDocEntries - -- Misc. pages -- e.g. ["http://myhackage.com/index", ...] miscEntries = urlsToSitemapEntries miscPages pageBuildDate Weekly 0.75 @@ -224,3 +269,26 @@ generateSitemap serverBaseURI pageBuildDate alltags pkgIndex docIndex = , Map.member (packageId pkg) docIndex ] pageBuildDate Monthly 0.25 + + -- Versioned doc pages in subdirectories + -- versionedSubDocURIs :: [path :: String] + -- e.g. ["http://myhackage.com/packages/mypackage-1.0.2/docs/Lib.html", ...] + versionedDocSubEntriesIO = do + let pkgs = [ (pkg , blob) + | pkg <- concat pkgss + , Just blob <- [Map.lookup (packageId pkg) docIndex] + ] + pkgIndices <- traverse (\(pkg, blob) -> (pkg,) <$> cachedTarIndex blob) pkgs + pure $ urlsToSitemapEntries + [ prefixPkgURI ++ display (packageId pkg) ++ "/docs" ++ fp + | (pkg, tarIndex) <- pkgIndices + , Just tar <- [Tar.lookup tarIndex ""] + , fp <- entryToPaths "/" tar + , takeExtension fp == ".html" + ] + pageBuildDate Monthly 0.25 + + entryToPaths :: FilePath -> Tar.TarIndexEntry -> [FilePath] + entryToPaths _ (Tar.TarFileEntry _) = [] + entryToPaths base (Tar.TarDir content) = map ((base ) . fst) content ++ + [ file | (folder, entry) <- content, file <- entryToPaths (base folder) entry ] diff --git a/src/Distribution/Server/Features/Sitemap/Functions.hs b/src/Distribution/Server/Features/Sitemap/Functions.hs index c13208c15..7eae7f0ec 100644 --- a/src/Distribution/Server/Features/Sitemap/Functions.hs +++ b/src/Distribution/Server/Features/Sitemap/Functions.hs @@ -23,6 +23,7 @@ module Distribution.Server.Features.Sitemap.Functions ( SitemapEntry , ChangeFreq(..) + , renderSitemapIndex , renderSitemap , urlsToSitemapEntries , pathsAndDatesToSitemapEntries @@ -47,6 +48,22 @@ data SitemapEntry = SitemapEntry { data ChangeFreq = Monthly | Weekly | Daily +-- | Generate a sitemap index file from each sitemap uri. +renderSitemapIndex :: URI -> [String] -> ByteString +renderSitemapIndex serverBaseURI sitemaps = + xrender $ + doc defaultDocInfo $ + xelem "sitemapindex" $ + xattr "xmlns" "http://www.sitemaps.org/schemas/sitemap/0.9" + <#> map renderLink sitemaps + where + serverBaseURI' = T.pack (show serverBaseURI) + renderLink :: String -> Xml Elem + renderLink uri = xelem "sitemap" $ + xelems [ + xelem "loc" (xtext (serverBaseURI' <> T.pack (uri))) + ] + -- | Primary function - generates the XML file from a list of Nodes. renderSitemap :: URI -> [SitemapEntry] -> ByteString renderSitemap serverBaseURI entries = diff --git a/src/Distribution/Server/Features/Tags.hs b/src/Distribution/Server/Features/Tags.hs index 6654aac82..d6c526e8e 100644 --- a/src/Distribution/Server/Features/Tags.hs +++ b/src/Distribution/Server/Features/Tags.hs @@ -39,7 +39,6 @@ import Data.Function (fix) import Data.List (foldl') import Data.Char (toLower) - data TagsFeature = TagsFeature { tagsFeatureInterface :: HackageFeature, @@ -64,6 +63,8 @@ data TagsFeature = TagsFeature { -- initial import. setCalculatedTag :: Tag -> Set PackageName -> IO (), + tagProposalLog :: MemState (Map PackageName (Set Tag, Set Tag)), + withTagPath :: forall a. DynamicPath -> (Tag -> Set PackageName -> ServerPartE a) -> ServerPartE a, collectTags :: forall m. MonadIO m => Set PackageName -> m (Map PackageName (Set Tag)), putTags :: Maybe String -> Maybe String -> Maybe String -> Maybe String -> PackageName -> ServerPartE (), @@ -97,9 +98,10 @@ initTagsFeature ServerEnv{serverStateDir} = do tagAlias <- tagsAliasComponent serverStateDir specials <- newMemStateWHNF emptyPackageTags updateTag <- newHook + tagProposalLog <- newMemStateWHNF Map.empty return $ \core@CoreFeature{..} upload user -> do - let feature = tagsFeature core upload user tagsState tagAlias specials updateTag + let feature = tagsFeature core upload user tagsState tagAlias specials updateTag tagProposalLog registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, mpkginfo) -> case mpkginfo of @@ -148,6 +150,7 @@ tagsFeature :: CoreFeature -> StateComponent AcidState TagAlias -> MemState PackageTags -> Hook (Set PackageName, Set Tag) () + -> MemState (Map PackageName (Set Tag, Set Tag)) -> TagsFeature tagsFeature CoreFeature{ queryGetPackageIndex } @@ -157,6 +160,7 @@ tagsFeature CoreFeature{ queryGetPackageIndex } tagsAlias calculatedTags tagsUpdated + tagProposalLog = TagsFeature{..} where tagsResource = fix $ \r -> TagsResource @@ -277,8 +281,10 @@ tagsFeature CoreFeature{ queryGetPackageIndex } Nothing -> [] addRev = Set.difference (fst revTags) (Set.fromList add `Set.union` Set.fromList radd') delRev = Set.difference (snd revTags) (Set.fromList del `Set.union` Set.fromList rdel') - void $ updateState tagsState $ SetPackageTags pkgname tagSet - void $ updateState tagsState $ InsertReviewTags' pkgname addRev delRev + modifyTags (a, d) = (a `Set.intersection` addRev, d `Set.intersection` delRev) + updateState tagsState $ SetPackageTags pkgname tagSet + updateState tagsState $ InsertReviewTags' pkgname addRev delRev + modifyMemState tagProposalLog (Map.adjust modifyTags pkgname) runHook_ tagsUpdated (Set.singleton pkgname, tagSet) return () else if user @@ -287,7 +293,9 @@ tagsFeature CoreFeature{ queryGetPackageIndex } calcTags <- queryTagsForPackage pkgname let addTags = Set.fromList aliases `Set.difference` calcTags delTags = Set.fromList del `Set.intersection` calcTags - void $ updateState tagsState $ InsertReviewTags pkgname addTags delTags + updateState tagsState $ InsertReviewTags pkgname addTags delTags + modifyMemState tagProposalLog (Map.insertWith (<>) pkgname (addTags, delTags)) + return () else errBadRequest "Authorization Error" [MText "You need to be logged in to propose tags"] _ -> errBadRequest "Tags not recognized" [MText "Couldn't parse your tag list. It should be comma separated with any number of alphanumerical tags. Tags can also also have -+#*."] Nothing -> errBadRequest "Tags not recognized" [MText "Couldn't parse your tag list. It should be comma separated with any number of alphanumerical tags. Tags can also also have -+#*."] diff --git a/src/Distribution/Server/Features/UserDetails.hs b/src/Distribution/Server/Features/UserDetails.hs index 04b5e750a..6240e9e8c 100644 --- a/src/Distribution/Server/Features/UserDetails.hs +++ b/src/Distribution/Server/Features/UserDetails.hs @@ -330,6 +330,7 @@ userDetailsFeature templates userDetailsState UserFeature{..} CoreFeature{..} Up handlerGetUserNameContactHtml :: DynamicPath -> ServerPartE Response handlerGetUserNameContactHtml dpath = do (uid, uinfo) <- lookupUserNameFull =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] template <- getTemplate templates "user-details-form.html" udetails <- queryUserDetails uid showConfirmationOfSave <- not . null <$> queryString (lookBSs "showConfirmationOfSave") diff --git a/src/Distribution/Server/Features/UserNotify.hs b/src/Distribution/Server/Features/UserNotify.hs new file mode 100644 index 000000000..bfcb0716a --- /dev/null +++ b/src/Distribution/Server/Features/UserNotify.hs @@ -0,0 +1,631 @@ +{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, + TypeFamilies, TemplateHaskell, + RankNTypes, NamedFieldPuns, RecordWildCards, BangPatterns, + DefaultSignatures, OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} +module Distribution.Server.Features.UserNotify ( + initUserNotifyFeature, + UserNotifyFeature(..), + NotifyPref(..), + ) where + +import Distribution.Package +import Distribution.Pretty + +import Distribution.Server.Users.Types(UserId, UserInfo (..)) +import Distribution.Server.Users.UserIdSet as UserIdSet +import qualified Distribution.Server.Users.Users as Users +import Distribution.Server.Users.Group + +import Distribution.Server.Packages.Types +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex + +import Distribution.Server.Framework +import Distribution.Server.Framework.Templating +import Distribution.Server.Framework.BackupDump +import Distribution.Server.Framework.BackupRestore + +import Distribution.Server.Features.AdminLog +import Distribution.Server.Features.BuildReports +import qualified Distribution.Server.Features.BuildReports.BuildReport as BuildReport +import Distribution.Server.Features.Core +import Distribution.Server.Features.Tags +import Distribution.Server.Features.Users +import Distribution.Server.Features.UserDetails +import Distribution.Server.Features.Upload + +import qualified Data.Map as Map +import qualified Data.Set as Set + +import Data.Typeable (Typeable) +import Control.Monad.Reader (ask) +import Control.Monad.State (get, put) +import Data.SafeCopy (base, deriveSafeCopy) +import Distribution.Text (display) +import Text.CSV (CSV, Record) +import Text.XHtml hiding (base, text, ()) +import Text.PrettyPrint +import Data.List(intercalate) +import Data.Hashable (Hashable(..)) +import Data.Aeson.TH ( defaultOptions, deriveJSON ) + +import Data.Time (UTCTime(..), getCurrentTime, diffUTCTime, addUTCTime, defaultTimeLocale, formatTime) +import Data.Time.Format.Internal (buildTime) + +import Data.Bifunctor ( Bifunctor(second) ) +import Data.Maybe(fromMaybe, mapMaybe, fromJust, listToMaybe, maybeToList) + +import Network.Mail.Mime +import Network.URI(uriAuthority, uriRegName) + +import qualified Data.ByteString.Lazy.Char8 as BS +import qualified Data.ByteString.Char8 as BSS +import qualified Data.Text as T +import qualified Data.Vector as Vec +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Types as Aeson + +-- A feature to manage notifications to users when package metadata, etc is updated. + +{- +Some missing features: + -- better formatting with mail templates +-} + +data UserNotifyFeature = UserNotifyFeature { + userNotifyFeatureInterface :: HackageFeature, + + queryGetUserNotifyPref :: forall m. MonadIO m => UserId -> m (Maybe NotifyPref), + updateSetUserNotifyPref :: forall m. MonadIO m => UserId -> NotifyPref -> m () +} + +instance IsHackageFeature UserNotifyFeature where + getFeatureInterface = userNotifyFeatureInterface + +------------------------- +-- Types of stored data +-- + +data NotifyPref = NotifyPref + { + notifyOptOut :: Bool, + notifyRevisionRange :: NotifyRevisionRange, + notifyUpload :: Bool, + notifyMaintainerGroup :: Bool, + notifyDocBuilderReport :: Bool, + notifyPendingTags :: Bool + } + deriving (Eq, Read, Show, Typeable) + +defaultNotifyPrefs :: NotifyPref +defaultNotifyPrefs = NotifyPref { + notifyOptOut = True, -- TODO when we're comfortable with this we can change to False. + notifyRevisionRange = NotifyAllVersions, + notifyUpload = True, + notifyMaintainerGroup = True, + notifyDocBuilderReport = True, + notifyPendingTags = True + } + +data NotifyRevisionRange = NotifyAllVersions | NotifyNewestVersion | NoNotifyRevisions deriving (Eq, Enum, Read, Show, Typeable) + +instance Pretty NotifyRevisionRange where + pretty NoNotifyRevisions = text "No" + pretty NotifyAllVersions = text "All Versions" + pretty NotifyNewestVersion = text "Newest Version" + +instance Hashable NotifyRevisionRange where + hash = fromEnum + hashWithSalt s x = s `hashWithSalt` hash x + +instance MemSize NotifyPref where memSize _ = memSize ((True,True,True),(True,True, True)) + +data NotifyData = NotifyData {unNotifyData :: (Map.Map UserId NotifyPref, UTCTime)} deriving (Eq, Show, Typeable) + +instance MemSize NotifyData where memSize (NotifyData x) = memSize x + +emptyNotifyData :: IO NotifyData +emptyNotifyData = getCurrentTime >>= \x-> return (NotifyData (Map.empty, x)) + +$(deriveSafeCopy 0 'base ''NotifyRevisionRange) +$(deriveSafeCopy 0 'base ''NotifyPref) +$(deriveSafeCopy 0 'base ''NotifyData) +$(deriveJSON defaultOptions ''NotifyRevisionRange) + +------------------------------ +-- UI +-- + +-- | `Bool`'s 'FromJSON' instance can't parse strings: +-- +-- >>> import qualified Data.Aeson as Aeson +-- >>> import qualified Data.ByteString.Lazy.Char8 as BS +-- >>> Aeson.decode (BS.pack "\"true\"") :: Maybe Bool +-- Nothing +-- +-- However, form2json will pass JSON bool values as strings to the decoder. +-- So we define a newtype wrapping it up. +newtype OK = OK {unOK :: Bool} deriving (Eq, Show, Enum) + +instance Pretty OK where + pretty (OK True) = text "Yes" + pretty (OK False) = text "No" + +instance Aeson.ToJSON OK where + toJSON = Aeson.toJSON . unOK + +instance Aeson.FromJSON OK where + parseJSON (Aeson.Bool b) = pure (OK b) + parseJSON (Aeson.String "true") = pure (OK True) + parseJSON (Aeson.String "false") = pure (OK False) + parseJSON s@(Aeson.String _) = Aeson.prependFailure "parsing OK failed, " (Aeson.unexpected s) + parseJSON invalid = Aeson.prependFailure "parsing OK failed, " (Aeson.typeMismatch "Bool or String" invalid) + +instance Hashable OK where + hashWithSalt s x = s `hashWithSalt` fromEnum x + +data NotifyPrefUI + = NotifyPrefUI + { ui_notifyEnabled :: OK + , ui_notifyRevisionRange :: NotifyRevisionRange + , ui_notifyUpload :: OK + , ui_notifyMaintainerGroup :: OK + , ui_notifyDocBuilderReport :: OK + , ui_notifyPendingTags :: OK + } + deriving (Eq, Show, Typeable) + +$(deriveJSON (compatAesonOptionsDropPrefix "ui_") ''NotifyPrefUI) + +instance Hashable NotifyPrefUI where + hashWithSalt s NotifyPrefUI{..} = s + `hashWithSalt` hash ui_notifyEnabled + `hashWithSalt` hash ui_notifyRevisionRange + `hashWithSalt` hash ui_notifyUpload + `hashWithSalt` hash ui_notifyMaintainerGroup + `hashWithSalt` hash ui_notifyDocBuilderReport + `hashWithSalt` hash ui_notifyPendingTags + +notifyPrefToUI :: NotifyPref -> NotifyPrefUI +notifyPrefToUI NotifyPref{..} = NotifyPrefUI + { ui_notifyEnabled = OK (not notifyOptOut) + , ui_notifyRevisionRange = notifyRevisionRange + , ui_notifyUpload = OK notifyUpload + , ui_notifyMaintainerGroup = OK notifyMaintainerGroup + , ui_notifyDocBuilderReport = OK notifyDocBuilderReport + , ui_notifyPendingTags = OK notifyPendingTags + } + +notifyPrefFromUI :: NotifyPrefUI -> NotifyPref +notifyPrefFromUI NotifyPrefUI{..} = NotifyPref + { notifyOptOut = not (unOK ui_notifyEnabled) + , notifyRevisionRange = ui_notifyRevisionRange + , notifyUpload = unOK ui_notifyUpload + , notifyMaintainerGroup = unOK ui_notifyMaintainerGroup + , notifyDocBuilderReport = unOK ui_notifyDocBuilderReport + , notifyPendingTags = unOK ui_notifyPendingTags + } + +class ToRadioButtons a where + toRadioButtons :: String -> a -> Html + +renderRadioButtons :: (Eq a, Aeson.ToJSON a, Pretty a) => [a] -> String -> a -> Html +renderRadioButtons choices nm def = foldr1 (+++) $ map renderRadioButton choices + where + renderRadioButton choice = toHtml + [ input ! (if (def == choice) then (checked :) else id) + [thetype "radio", identifier htmlId, name nm, value choiceName] + , label ! [thefor htmlId] << display choice + ] + where + jsonName = Aeson.encode choice + -- try to strip quotes + choiceName = BS.unpack $ if BS.head jsonName == '"' && BS.last jsonName == '"' + then BS.init (BS.tail jsonName) + else jsonName + htmlId = nm ++ "." ++ choiceName + +instance ToRadioButtons NotifyRevisionRange where + toRadioButtons = renderRadioButtons [NoNotifyRevisions, NotifyAllVersions, NotifyNewestVersion] + +instance ToRadioButtons OK where + toRadioButtons = renderRadioButtons [OK True, OK False] + +------------------------------ +-- State queries and updates +-- + +getNotifyData :: Query NotifyData NotifyData +getNotifyData = ask + +replaceNotifyData :: NotifyData -> Update NotifyData () +replaceNotifyData = put + +getNotifyTime :: Query NotifyData UTCTime +getNotifyTime = fmap (snd . unNotifyData) ask + +setNotifyTime :: UTCTime -> Update NotifyData () +setNotifyTime t = do + NotifyData (m,_) <- get + put $! NotifyData (m,t) + +lookupNotifyPref :: UserId -> Query NotifyData (Maybe NotifyPref) +lookupNotifyPref uid = do + NotifyData (m,_) <- ask + return $! Map.lookup uid m + +addNotifyPref :: UserId -> NotifyPref -> Update NotifyData () +addNotifyPref uid info = do + NotifyData (m,t) <- get + put $! NotifyData (Map.insert uid info m,t) + +makeAcidic ''NotifyData [ + --queries + 'getNotifyData, + 'lookupNotifyPref, + 'getNotifyTime, + --updates + 'replaceNotifyData, + 'addNotifyPref, + 'setNotifyTime + ] + + +---------------------------- +-- Data backup and restore +-- + +userNotifyBackup :: RestoreBackup NotifyData +userNotifyBackup = go [] + where + go :: [(UserId, NotifyPref)] -> RestoreBackup NotifyData + go st = + RestoreBackup { + restoreEntry = \entry -> case entry of + BackupByteString ["notifydata.csv"] bs -> do + csv <- importCSV "notifydata.csv" bs + prefs <- importNotifyPref csv + return (go (prefs ++ st)) + + _ -> return (go st) + + , restoreFinalize = + return (NotifyData (Map.fromList st, fromJust (buildTime defaultTimeLocale []))) -- defaults to unixstart time + } + +importNotifyPref :: CSV -> Restore [(UserId, NotifyPref)] +importNotifyPref = sequence . map fromRecord . drop 2 + where + fromRecord :: Record -> Restore (UserId, NotifyPref) + fromRecord [uid,o,rr,ul,g,db,t] = do + puid <- parseText "user id" uid + po <- parseRead "notify opt out" o + prr <- parseRead "notify revsion" rr + pul <- parseRead "notify upload" ul + pg <- parseRead "notify group mod" g + pd <- parseRead "notify docbuilder" db + pt <- parseRead "notify pending tags" t + return (puid, NotifyPref po prr pul pg pd pt) + fromRecord x = fail $ "Error processing notify record: " ++ show x + +notifyDataToCSV :: BackupType -> NotifyData -> CSV +notifyDataToCSV _backuptype (NotifyData (tbl,_)) + = ["0.1"] + : [ "uid","freq","revisionrange","upload","group"] + : flip map (Map.toList tbl) (\(uid,np) -> + [display uid, show (notifyOptOut np), show (notifyRevisionRange np), show (notifyUpload np), show (notifyMaintainerGroup np), show (notifyDocBuilderReport np), show (notifyPendingTags np)] + ) + +---------------------------- +-- State Component +-- + +notifyStateComponent :: FilePath -> IO (StateComponent AcidState NotifyData) +notifyStateComponent stateDir = do + st <- openLocalStateFrom (stateDir "db" "UserNotify") =<< emptyNotifyData + return StateComponent { + stateDesc = "State to keep track of revision notifications" + , stateHandle = st + , getState = query st GetNotifyData + , putState = update st . ReplaceNotifyData + , backupState = \backuptype tbl -> + [csvToBackup ["notifydata.csv"] (notifyDataToCSV backuptype tbl)] + , restoreState = userNotifyBackup + , resetState = notifyStateComponent + } + +---------------------------- +-- Core Feature +-- + +initUserNotifyFeature :: ServerEnv + -> IO (UserFeature + -> CoreFeature + -> UploadFeature + -> AdminLogFeature + -> UserDetailsFeature + -> ReportsFeature + -> TagsFeature + -> IO UserNotifyFeature) +initUserNotifyFeature env@ServerEnv{ serverStateDir, serverTemplatesDir, + serverTemplatesMode } = do + -- Canonical state + notifyState <- notifyStateComponent serverStateDir + + -- Page templates + templates <- loadTemplates serverTemplatesMode + [serverTemplatesDir, serverTemplatesDir "UserNotify"] + [ "user-notify-form.html" ] + + return $ \users core uploadfeature adminlog userdetails reports tags -> do + let feature = userNotifyFeature env + users core uploadfeature adminlog userdetails reports tags + notifyState templates + return feature + + +userNotifyFeature :: ServerEnv + -> UserFeature + -> CoreFeature + -> UploadFeature + -> AdminLogFeature + -> UserDetailsFeature + -> ReportsFeature + -> TagsFeature + -> StateComponent AcidState NotifyData + -> Templates + -> UserNotifyFeature +userNotifyFeature ServerEnv{serverBaseURI, serverCron} + UserFeature{..} + CoreFeature{..} + UploadFeature{..} + AdminLogFeature{..} + UserDetailsFeature{..} + ReportsFeature{..} + TagsFeature{..} + notifyState templates + = UserNotifyFeature {..} + + where + userNotifyFeatureInterface = (emptyHackageFeature "user-notify") { + featureDesc = "Notifications to users on metadata updates." + , featureResources = [userNotifyResource] -- TODO we can add json features here for updating prefs + , featureState = [abstractAcidStateComponent notifyState] + , featureCaches = [] + , featureReloadFiles = reloadTemplates templates + , featurePostInit = setupNotifyCronJob + } + + -- Resources + -- + + userNotifyResource = + (resourceAt "/user/:username/notify.:format") { + resourceDesc = [ (GET, "get the notify preference of a user account") + , (PUT, "set the notify preference of a user account") + ] + , resourceGet = [ ("json", handlerGetUserNotify) + , ("html", handlerGetUserNotifyHtml) + ] + , resourcePut = [ ("json", handlerPutUserNotify) ] + } + + -- Queries and updates + -- + + queryGetUserNotifyPref :: MonadIO m => UserId -> m (Maybe NotifyPref) + queryGetUserNotifyPref uid = queryState notifyState (LookupNotifyPref uid) + + updateSetUserNotifyPref :: MonadIO m => UserId -> NotifyPref -> m () + updateSetUserNotifyPref uid np = updateState notifyState (AddNotifyPref uid np) + + -- Request handlers + -- + handlerGetUserNotify dpath = do + uid <- lookupUserName =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] + nprefui <- notifyPrefToUI . fromMaybe defaultNotifyPrefs <$> queryGetUserNotifyPref uid + return $ toResponse (Aeson.toJSON nprefui) + + handlerGetUserNotifyHtml dpath = do + (uid, uinfo) <- lookupUserNameFull =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] + nprefui@NotifyPrefUI{..} <- notifyPrefToUI . fromMaybe defaultNotifyPrefs <$> queryGetUserNotifyPref uid + showConfirmationOfSave <- not . Prelude.null <$> queryString (lookBSs "showConfirmationOfSave") + template <- getTemplate templates "user-notify-form.html" + cacheControl [Private] $ etagFromHash (nprefui, showConfirmationOfSave) + ok . toResponse $ + template + [ "username" $= display (userName uinfo) + , "showConfirmationOfSave" $= showConfirmationOfSave + , "notifyEnabled" $= toRadioButtons "notifyEnabled=%s" ui_notifyEnabled + , "notifyRevisionRange" $= toRadioButtons "notifyRevisionRange=%s" ui_notifyRevisionRange + , "notifyUpload" $= toRadioButtons "notifyUpload=%s" ui_notifyUpload + , "notifyMaintainerGroup" $= toRadioButtons "notifyMaintainerGroup=%s" ui_notifyMaintainerGroup + , "notifyDocBuilderReport" $= toRadioButtons "notifyDocBuilderReport=%s" ui_notifyDocBuilderReport + , "notifyPendingTags" $= toRadioButtons "notifyPendingTags=%s" ui_notifyPendingTags + ] + + handlerPutUserNotify dpath = do + uid <- lookupUserName =<< userNameInPath dpath + guardAuthorised_ [IsUserId uid, InGroup adminGroup] + nprefui <- expectAesonContent + updateSetUserNotifyPref uid (notifyPrefFromUI nprefui) + noContent $ toResponse () + + -- Engine + -- + setupNotifyCronJob = + addCronJob serverCron CronJob { + cronJobName = "send notifications", + cronJobFrequency = TestJobFrequency (60*60*2), -- 2hr (for testing you can decrease this) + cronJobOneShot = False, + cronJobAction = notifyCronAction + } + + notifyCronAction = do + (notifyPrefs, lastNotifyTime) <- unNotifyData <$> queryState notifyState GetNotifyData + now <- getCurrentTime + let trimLastTime = if diffUTCTime now lastNotifyTime > (60*60*6) -- cap at 6hr + then addUTCTime (negate $ (60*60*6)) now + else lastNotifyTime -- for testing you can increase this + users <- queryGetUserDb + + revisionsAndUploads <- collectRevisionsAndUploads trimLastTime now + revisionUploadNotifications <- foldM (genRevUploadList notifyPrefs) Map.empty revisionsAndUploads + let revisionUploadEmails = map (describeRevision users trimLastTime now) <$> revisionUploadNotifications + + groupActions <- collectAdminActions trimLastTime now + groupActionNotifications <- foldM (genGroupUploadList notifyPrefs) Map.empty groupActions + let groupActionEmails = mapMaybe (describeGroupAction users) <$> groupActionNotifications + + docReports <- collectDocReport trimLastTime now + docReportNotifications <- foldM (genDocReportList notifyPrefs) Map.empty docReports + let docReportEmails = map describeDocReport <$> docReportNotifications + + tagProposals <- collectTagProposals + tagProposalNotifications <- foldM (genTagProposalList notifyPrefs) Map.empty tagProposals + let tagProposalEmails = map describeTagProposal <$> tagProposalNotifications + + mapM_ sendNotifyEmail . Map.toList $ foldr1 (Map.unionWith (++)) [revisionUploadEmails, groupActionEmails, docReportEmails, tagProposalEmails] + updateState notifyState (SetNotifyTime now) + + formatTimeUser users t u = + display (Users.userIdToName users u) ++ " [" ++ + (formatTime defaultTimeLocale "%c" t) ++ "]" + + collectRevisionsAndUploads earlier now = do + pkgIndex <- queryGetPackageIndex + let isRecent pkgInfo = + let rt = pkgLatestUploadTime pkgInfo + in rt > earlier && rt <= now + return $ filter isRecent $ (PackageIndex.allPackages pkgIndex) + + collectAdminActions earlier now = do + aLog <- adminLog <$> queryGetAdminLog + let isRecent (t,_,_,_) = t > earlier && t <= now + return $ filter isRecent $ aLog + + collectDocReport earlier now = do + pkgs <- PackageIndex.allPackages <$> queryGetPackageIndex + pkgRpts <- forM pkgs $ \pkg -> do + rpts <- queryPackageReports (packageId pkg) + pure $ (pkg,) $ do + -- List monad, filter out recent docbuilds + (_, rpt@BuildReport.BuildReport{..}) <- rpts + t <- maybeToList time + guard $ docsOutcome /= BuildReport.NotTried && t > earlier && t <= now + pure rpt + let isBuildOk BuildReport.BuildReport{..} = docsOutcome == BuildReport.Ok + pure $ map (second (all isBuildOk)) $ filter (not . Prelude.null . snd) pkgRpts + + collectTagProposals = do + logs <- readMemState tagProposalLog + writeMemState tagProposalLog Map.empty + pure $ Map.toList logs + + genRevUploadList notifyPrefs mp pkg = do + pkgIndex <- queryGetPackageIndex + let actor = pkgLatestUploadUser pkg + isRevision = pkgNumRevisions pkg > 1 + pkgName = packageName . pkgInfoId $ pkg + mbLatest = listToMaybe . take 1 . reverse $ PackageIndex.lookupPackageName pkgIndex pkgName + isLatestVersion = maybe False (\x -> pkgInfoId pkg == pkgInfoId x) mbLatest + addNotification uid m = + if not (notifyOptOut npref) && + (isRevision && + ( notifyRevisionRange npref == NotifyAllVersions || + ((notifyRevisionRange npref == NotifyNewestVersion) && isLatestVersion)) + || + not isRevision && notifyUpload npref) + then Map.insertWith (++) uid [pkg] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + maintainers <- queryUserGroup $ maintainersGroup (packageName . pkgInfoId $ pkg) + return $ foldr addNotification mp (toList (delete actor maintainers)) + + genGroupUploadList notifyPrefs mp ga = + let (actor,gdesc) = case ga of (_,uid,Admin_GroupAddUser _ gd,_) -> (uid, gd) + (_,uid,Admin_GroupDelUser _ gd,_) -> (uid, gd) + addNotification uid m = if not (notifyOptOut npref) && notifyMaintainerGroup npref + then Map.insertWith (++) uid [ga] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + in case gdesc of + (MaintainerGroup pkg) -> do + maintainers <- queryUserGroup $ maintainersGroup (mkPackageName $ BS.unpack pkg) + return $ foldr addNotification mp (toList (delete actor maintainers)) + _ -> return mp + + genDocReportList notifyPrefs mp pkgDoc = do + let addNotification uid m = + if not (notifyOptOut npref) && notifyDocBuilderReport npref + then Map.insertWith (++) uid [pkgDoc] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + maintainers <- queryUserGroup $ maintainersGroup (packageName . pkgInfoId . fst $ pkgDoc) + return $ foldr addNotification mp (toList maintainers) + + genTagProposalList notifyPrefs mp pkgTags = do + let addNotification uid m = + if not (notifyOptOut npref) && notifyPendingTags npref + then Map.insertWith (++) uid [pkgTags] m + else m + where npref = fromMaybe defaultNotifyPrefs (Map.lookup uid notifyPrefs) + maintainers <- queryUserGroup $ maintainersGroup (fst pkgTags) + return $ foldr addNotification mp (toList maintainers) + + describeRevision users earlier now pkg = + if pkgNumRevisions pkg <= 1 + then "Package upload, " ++ display (packageName pkg) ++ ", by " ++ + formatTimeUser users (pkgLatestUploadTime pkg) (pkgLatestUploadUser pkg) + else "Package metadata revision(s), " ++ display (packageName pkg) ++ ":\n" ++ + unlines (map (uncurry (formatTimeUser users) . snd) recentRevs) + where + revs = reverse $ Vec.toList (pkgMetadataRevisions pkg) + recentRevs = filter ((\x -> x > earlier && x <= now) . fst . snd) revs + + describeGroupAction users (time, uid, act, descr) = + case act of + (Admin_GroupAddUser tn (MaintainerGroup pkg)) -> Just $ + "Group modified by " ++ formatTimeUser users time uid ++ ":\n" ++ + display (Users.userIdToName users tn) ++ " added to maintainers for " ++ BS.unpack pkg ++ + "\n" ++ "reason: " ++ BS.unpack descr + (Admin_GroupDelUser tn (MaintainerGroup pkg)) -> Just $ + "Group modified by " ++ formatTimeUser users time uid ++ ":\n" ++ + display (Users.userIdToName users tn) ++ " removed from maintainers for " ++ BS.unpack pkg ++ + "\n" ++ "reason: " ++ BS.unpack descr + _ -> Nothing + + describeDocReport (pkg, doc) = + "Package doc build for " ++ display (packageName pkg) ++ ":\n" ++ + if doc + then "Build successful." + else "Build failed." + + describeTagProposal (pkgName, (addTags, delTags)) = + "Pending tag propasal for " ++ display pkgName ++ ":\n" ++ + "Addition: " ++ showTags addTags ++ "\n" ++ + "Deletion: " ++ showTags delTags + where + showTags = intercalate ", " . map display . Set.toList + + sendNotifyEmail :: (UserId, [String]) -> IO () + sendNotifyEmail (uid, ebody) = do + mudetails <- queryUserDetails uid + case mudetails of + Nothing -> return () + Just (AccountDetails{accountContactEmail=eml, accountName=aname})-> do + let mailFrom = Address (Just (T.pack "Hackage website")) + (T.pack ("noreply@" ++ uriRegName ourHost)) + mail = (emptyMail mailFrom) { + mailTo = [Address (Just aname) eml], + mailHeaders = [(BSS.pack "Subject", + T.pack "[Hackage] Maintainer Notifications")], + mailParts = [[Part (T.pack "text/plain; charset=utf-8") + None DefaultDisposition [] (PartContent $ BS.pack $ intercalate ("\n\n") ebody)]] + } + Just ourHost = uriAuthority serverBaseURI + + renderSendMail mail --TODO: if we need any configuration of + -- sendmail stuff, has to go here diff --git a/src/Distribution/Server/Features/UserSignup.hs b/src/Distribution/Server/Features/UserSignup.hs index 7ab895331..0fb716c8e 100644 --- a/src/Distribution/Server/Features/UserSignup.hs +++ b/src/Distribution/Server/Features/UserSignup.hs @@ -28,18 +28,28 @@ import Data.Map (Map) import qualified Data.Map as Map import Data.Text (Text) import qualified Data.Text as T +import qualified Data.Text.Encoding as T import qualified Data.ByteString.Char8 as BS -- Only used for ASCII data - +import qualified Data.ByteString.Lazy as BSL import Data.Typeable (Typeable) import Control.Monad.Reader (ask) import Control.Monad.State (get, put, modify) import Data.SafeCopy (base, deriveSafeCopy) import Distribution.Text (display) -import Data.Time (UTCTime(..), getCurrentTime, addDays) +import Data.Time import Text.CSV (CSV, Record) import Network.Mail.Mime import Network.URI (URI(..), URIAuth(..)) +import Graphics.Captcha +import qualified Data.ByteString.Base64 as Base64 +import qualified Crypto.Hash.SHA256 as SHA256 +import Data.String +import Data.Char +import Text.Read (readMaybe) +import Data.Aeson +import qualified Data.Aeson.KeyMap as KeyMap +import qualified Data.Aeson.Key as Key -- | A feature to allow open account signup, and password reset, @@ -306,6 +316,7 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron} userSignupFeatureInterface = (emptyHackageFeature "user-signup-reset") { featureDesc = "Extra information about user accounts, email addresses etc." , featureResources = [signupRequestsResource, + captchaResource, signupRequestResource, resetRequestsResource, resetRequestResource] @@ -326,6 +337,12 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron} , resourcePost = [ ("", handlerPostSignupRequestNew) ] } + captchaResource = + (resourceAt "/users/register/captcha") { + resourceDesc = [ (GET, "Get a new captcha") ] + , resourceGet = [ ("json", handlerGetCaptcha) ] + } + signupRequestResource = (resourceAt "/users/register-request/:nonce") { resourceDesc = [ (GET, "Page for confirming outstanding signup request") @@ -413,20 +430,44 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron} [MText $ "The " ++ thing ++ " token does not exist. It could be that it " ++ "has been used already, or that it has expired."] + hashTimeAndCaptcha :: UTCTime -> String -> BS.ByteString + hashTimeAndCaptcha timestamp captcha = Base64.encode (SHA256.hash (fromString (show timestamp ++ map toUpper captcha))) + + makeCaptchaHash :: IO (UTCTime, BS.ByteString, BS.ByteString) + makeCaptchaHash = do + (code, image) <- makeCaptcha + timestamp <- getCurrentTime + pure (timestamp, hashTimeAndCaptcha timestamp code, fromString "data:image/png;base64," <> Base64.encode image) + handlerGetSignupRequestNew :: DynamicPath -> ServerPartE Response handlerGetSignupRequestNew _ = do + (timestamp, hash, base64image) <- liftIO makeCaptchaHash template <- getTemplate templates "SignupRequest.html" - ok $ toResponse $ template [] + ok $ toResponse $ template + [ "timestamp" $= timestamp + , "hash" $= hash + , "base64image" $= base64image + ] + + handlerGetCaptcha :: DynamicPath -> ServerPartE Response + handlerGetCaptcha _ = do + (timestamp, hash, base64image) <- liftIO makeCaptchaHash + ok $ toResponse $ Object $ KeyMap.fromList $ + [ (Key.fromString "timestamp" , String (T.pack (show timestamp))) + , (Key.fromString "hash" , String (T.decodeUtf8 hash)) + , (Key.fromString "base64image", String (T.decodeUtf8 base64image)) + ] handlerPostSignupRequestNew :: DynamicPath -> ServerPartE Response handlerPostSignupRequestNew _ = do templateEmail <- getTemplate templates "SignupConfirmation.email" templateConfirmation <- getTemplate templates "SignupEmailSent.html" - (username, realname, useremail) <- lookUserNameEmail + timestamp <- liftIO getCurrentTime + + (username, realname, useremail) <- lookValidFields timestamp nonce <- liftIO (newRandomNonce 10) - timestamp <- liftIO getCurrentTime let signupInfo = SignupInfo { signupUserName = username, signupRealName = realname, @@ -462,17 +503,29 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron} templateConfirmation [ "useremail" $= useremail ] where - lookUserNameEmail = do - (username, realname, useremail) <- - msum [ body $ (,,) <$> lookText' "username" - <*> lookText' "realname" - <*> lookText' "email" + lookValidFields now = do + (username, realname, useremail, captcha, timestampStr, hash) <- + msum [ body $ (,,,,,) <$> lookText' "username" + <*> lookText' "realname" + <*> lookText' "email" + <*> look "captcha" + <*> look "timestamp" + <*> lookBS "hash" , errBadRequest "Missing form fields" [] ] guardValidLookingUserName username guardValidLookingName realname guardValidLookingEmail useremail + timestamp <- maybe (errBadRequest "Invalid request" [MText "Seems something went wrong with your request."]) + pure (readMaybe timestampStr) + + when (diffUTCTime now timestamp > secondsToNominalDiffTime (10 * 60)) $ + errBadRequest "Problem with captcha" [MText "Oops, The captcha has expired. Please be quick next time!"] + + unless (hashTimeAndCaptcha timestamp captcha == BSL.toStrict hash) $ + errBadRequest "Problem with captcha" [MText "Sorry, the captcha is wrong. Please try sign up again."] + return (username, realname, useremail) handlerGetSignupRequestOutstanding :: DynamicPath -> ServerPartE Response diff --git a/src/Distribution/Server/Framework/BackupRestore.hs b/src/Distribution/Server/Framework/BackupRestore.hs index 64e5c0bfa..48edeab17 100644 --- a/src/Distribution/Server/Framework/BackupRestore.hs +++ b/src/Distribution/Server/Framework/BackupRestore.hs @@ -51,8 +51,8 @@ import Distribution.Server.Features.Security.SHA256 import qualified Codec.Archive.Tar as Tar import qualified Codec.Archive.Tar.Entry as Tar import Distribution.Server.Util.GZip (decompressNamed) -import Control.Monad.State -import Control.Monad.Except +import Control.Monad.State (StateT, evalStateT, MonadState, get, gets, put) +import Control.Monad.Except (ExceptT, runExceptT, MonadError(..)) import Data.Time (UTCTime) import qualified Data.Time as Time import Data.Time.Format (defaultTimeLocale) @@ -251,7 +251,7 @@ data Restore a = RestoreDone a | RestoreFindBlob BlobId (Bool -> Restore a) instance Monad Restore where - return = RestoreDone + return = pure RestoreDone x >>= g = g x RestoreFail err >>= _ = RestoreFail err RestoreAddBlob bs f >>= g = RestoreAddBlob bs $ \bid -> f bid >>= g @@ -270,7 +270,7 @@ instance Functor Restore where fmap = liftM instance Applicative Restore where - pure = return + pure = RestoreDone mf <*> mx = do f <- mf ; x <- mx ; return (f x) runRestore :: BlobStores -> Restore a -> IO (Either String a) diff --git a/src/Distribution/Server/Framework/HappstackUtils.hs b/src/Distribution/Server/Framework/HappstackUtils.hs index 41bce1b74..f10491f08 100644 --- a/src/Distribution/Server/Framework/HappstackUtils.hs +++ b/src/Distribution/Server/Framework/HappstackUtils.hs @@ -81,6 +81,8 @@ mime x = , ("chs", "text/plain; charset=utf-8") , ("c", " text/plain; charset=utf-8") , ("h", " text/plain; charset=utf-8") + , ("text", "text/plain; charset=utf-8") + , ("txt", "text/plain; charset=utf-8") ] diff --git a/src/Distribution/Server/Framework/Instances.hs b/src/Distribution/Server/Framework/Instances.hs index 442e9c15f..d2a68cf57 100644 --- a/src/Distribution/Server/Framework/Instances.hs +++ b/src/Distribution/Server/Framework/Instances.hs @@ -136,6 +136,7 @@ instance SafeCopy OS where putCopy Ghcjs = contain $ putWord8 14 putCopy Hurd = contain $ putWord8 15 putCopy Android = contain $ putWord8 16 + putCopy Wasi = contain $ putWord8 17 getCopy = contain $ do tag <- getWord8 @@ -157,6 +158,7 @@ instance SafeCopy OS where 14 -> return Ghcjs 15 -> return Hurd 16 -> return Android + 17 -> return Wasi _ -> fail "SafeCopy OS getCopy: unexpected tag" instance SafeCopy Arch where @@ -180,6 +182,8 @@ instance SafeCopy Arch where putCopy Vax = contain $ putWord8 15 putCopy JavaScript = contain $ putWord8 16 putCopy AArch64 = contain $ putWord8 17 + putCopy S390X = contain $ putWord8 18 + putCopy Wasm32 = contain $ putWord8 19 getCopy = contain $ do tag <- getWord8 @@ -202,6 +206,8 @@ instance SafeCopy Arch where 15 -> return Vax 16 -> return JavaScript 17 -> return AArch64 + 18 -> return S390X + 19 -> return Wasm32 _ -> fail "SafeCopy Arch getCopy: unexpected tag" instance SafeCopy CompilerFlavor where diff --git a/src/Distribution/Server/Framework/MemSize.hs b/src/Distribution/Server/Framework/MemSize.hs index 4af5d251f..d98e00085 100644 --- a/src/Distribution/Server/Framework/MemSize.hs +++ b/src/Distribution/Server/Framework/MemSize.hs @@ -17,6 +17,10 @@ import Data.Set (Set) import qualified Data.IntSet as IntSet import Data.IntSet (IntSet) import Data.Sequence (Seq) +import qualified Data.Bimap as Bimap +import Data.Bimap (Bimap) +import qualified Data.Graph as Gr +import Data.Graph (Graph) import qualified Data.Foldable as Foldable import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS @@ -223,6 +227,12 @@ instance MemSize e => MemSize (V.Vector e) where memSizeUVector :: V.U.Unbox e => Int -> V.U.Vector e -> Int memSizeUVector sz a = 5 + (V.U.length a * sz) `div` wordSize +instance (MemSize a, MemSize b) => MemSize (Bimap a b) where + memSize m = sum [ 6 + memSize k + memSize v | (k,v) <- Bimap.toList m ] + +instance MemSize Graph where + memSize m = sum [ 6 + memSize v | v <- Gr.edges m ] + ---- diff --git a/src/Distribution/Server/Framework/ResponseContentTypes.hs b/src/Distribution/Server/Framework/ResponseContentTypes.hs index f42fee51c..fd20d7fe2 100644 --- a/src/Distribution/Server/Framework/ResponseContentTypes.hs +++ b/src/Distribution/Server/Framework/ResponseContentTypes.hs @@ -180,6 +180,12 @@ instance ToMessage BuildLog where toContentType _ = "text/plain" toMessage (BuildLog bs) = bs +newtype TestLog = TestLog BS.Lazy.ByteString + +instance ToMessage TestLog where + toContentType _ = "text/plain" + toMessage (TestLog bs) = bs + newtype BuildCovg = BuildCovg BS.Lazy.ByteString instance ToMessage BuildCovg where diff --git a/src/Distribution/Server/Packages/Index.hs b/src/Distribution/Server/Packages/Index.hs index e2f041de8..4adaf0b3c 100644 --- a/src/Distribution/Server/Packages/Index.hs +++ b/src/Distribution/Server/Packages/Index.hs @@ -207,10 +207,6 @@ writeLegacy users = -- -- This used to live in Distribution.Server.Util.Index. -- --- NOTE: In order to mitigate the effects of --- https://github.com/haskell/cabal/issues/4624 --- as a hack, this operation filters out .cabal files --- with cabal-version >= 2. writeLegacyAux :: Package pkg => (pkg -> ByteString) -> (pkg -> Tar.Entry -> Tar.Entry) @@ -218,21 +214,16 @@ writeLegacyAux :: Package pkg -> PackageIndex pkg -> ByteString writeLegacyAux externalPackageRep updateEntry extras = - Tar.write . (extras++) . mapMaybe entry . PackageIndex.allPackages + Tar.write . (extras++) . map entry . PackageIndex.allPackages where -- entry :: pkg -> Maybe Tar.Entry - entry pkg - | specVerGEq2 = Nothing - | otherwise = Just - . updateEntry pkg - . Tar.fileEntry tarPath - $ cabalText + entry pkg = + updateEntry pkg + . Tar.fileEntry tarPath + $ cabalText where Right tarPath = Tar.toTarPath False fileName name = unPackageName $ packageName pkg fileName = name display (packageVersion pkg) name <.> "cabal" - - -- TODO: Hack-alert! We want to do this in a more elegant way. - specVerGEq2 = maybe False (>= CabalSpecV2_0) $ parseSpecVerLazy cabalText cabalText = externalPackageRep pkg diff --git a/src/Distribution/Server/Packages/PackageIndex.hs b/src/Distribution/Server/Packages/PackageIndex.hs index 4b862d649..228e5b768 100644 --- a/src/Distribution/Server/Packages/PackageIndex.hs +++ b/src/Distribution/Server/Packages/PackageIndex.hs @@ -44,7 +44,8 @@ module Distribution.Server.Packages.PackageIndex ( -- ** Bulk queries allPackageNames, allPackages, - allPackagesByName + allPackagesByName, + allPackagesByNameNE ) where import Distribution.Server.Prelude hiding (lookup) @@ -58,6 +59,7 @@ import qualified Data.Map.Strict as Map import Data.Map.Strict (Map) import qualified Data.Foldable as Foldable import Data.List (groupBy, find, isInfixOf) +import qualified Data.List.NonEmpty as NE import Data.SafeCopy import Distribution.Types.PackageName @@ -255,6 +257,9 @@ allPackages (PackageIndex m) = concat (Map.elems m) -- -- They are grouped by package name, case-sensitively. -- +allPackagesByNameNE :: Package pkg => PackageIndex pkg -> [NE.NonEmpty pkg] +allPackagesByNameNE (PackageIndex m) = map NE.fromList $ Map.elems m + allPackagesByName :: Package pkg => PackageIndex pkg -> [[pkg]] allPackagesByName (PackageIndex m) = Map.elems m diff --git a/src/Distribution/Server/Packages/Render.hs b/src/Distribution/Server/Packages/Render.hs index 09c0577da..8b40303e5 100644 --- a/src/Distribution/Server/Packages/Render.hs +++ b/src/Distribution/Server/Packages/Render.hs @@ -149,6 +149,7 @@ doPackageRender users info = PackageRender = let mod_ix = mkForest $ exposedModules lib -- Assumes that there is an HTML per reexport ++ map moduleReexportName (reexportedModules lib) + ++ virtualModules (libBuildInfo lib) sig_ix = mkForest $ signatures lib mkForest = moduleForest . map (\m -> (m, moduleHasDocs docindex m)) in Just (ModSigIndex { modIndex = mod_ix, sigIndex = sig_ix }) diff --git a/src/Distribution/Server/Packages/Unpack.hs b/src/Distribution/Server/Packages/Unpack.hs index 60b273a23..e0d8c5e60 100644 --- a/src/Distribution/Server/Packages/Unpack.hs +++ b/src/Distribution/Server/Packages/Unpack.hs @@ -42,6 +42,7 @@ import Distribution.Text -- import qualified Distribution.Compat.CharParsing as P import Distribution.Server.Util.ParseSpecVer import qualified Distribution.SPDX as SPDX +import qualified Distribution.SPDX.LicenseId as SPDX.LId import qualified Distribution.License as License import Control.Monad.Except @@ -491,7 +492,7 @@ isAcceptableLicense = either goSpdx goLegacy . licenseRaw goSimple (SPDX.ELicenseRef _) = False -- don't allow referenced licenses goSimple (SPDX.ELicenseIdPlus _) = False -- don't allow + licenses (use GPL-3.0-or-later e.g.) goSimple (SPDX.ELicenseId SPDX.CC0_1_0) = True -- CC0 isn't OSI approved, but we allow it as "PublicDomain", this is eg. PublicDomain in http://hackage.haskell.org/package/string-qq-0.0.2/src/LICENSE - goSimple (SPDX.ELicenseId lid) = SPDX.licenseIsOsiApproved lid -- allow only OSI approved licenses. + goSimple (SPDX.ELicenseId lid) = SPDX.licenseIsOsiApproved lid || SPDX.LId.licenseIsFsfLibre lid -- allow only OSI or FSF approved licenses. -- pre `cabal-version: 2.2` goLegacy License.AllRightsReserved = False diff --git a/src/Distribution/Server/Pages/Group.hs b/src/Distribution/Server/Pages/Group.hs index 3afc3a035..cf7dcd5da 100644 --- a/src/Distribution/Server/Pages/Group.hs +++ b/src/Distribution/Server/Pages/Group.hs @@ -1,7 +1,8 @@ -- Body of the HTML page for a package module Distribution.Server.Pages.Group ( groupPage, - renderGroupName + renderGroupName, + listGroupCompact -- renderGroupNameWithCands ) where @@ -69,7 +70,13 @@ removeUser uname uri = ] listGroup :: [Users.UserName] -> Maybe String -> Html -listGroup [] _ = p << "No member exist presently" -listGroup users muri = unordList (map displayName users) - where displayName uname = (anchor ! [href $ "/user/" ++ display uname] << display uname) +++ +listGroup [] _ = p << "No current members of group" +listGroup users muri = unordList (map (displayName muri) users) + +listGroupCompact :: [Users.UserName] -> Html +listGroupCompact [] = toHtml "No current members of group" +listGroupCompact users = foldr1 (\a b -> a +++ ", " +++ b) (map (displayName Nothing) users) + +displayName :: Maybe String -> Users.UserName -> Html +displayName muri uname = (anchor ! [href $ "/user/" ++ display uname] << display uname) +++ maybe [] (removeUser uname) muri diff --git a/src/Distribution/Server/Pages/Package.hs b/src/Distribution/Server/Pages/Package.hs index 6bc46205a..5813355e1 100644 --- a/src/Distribution/Server/Pages/Package.hs +++ b/src/Distribution/Server/Pages/Package.hs @@ -34,7 +34,7 @@ import Distribution.Utils.ShortText (fromShortText, ShortText) import Text.XHtml.Strict hiding (p, name, title, content) import qualified Text.XHtml.Strict -import Data.Maybe (fromMaybe, maybeToList, isJust, mapMaybe) +import Data.Maybe (fromMaybe, maybeToList, isJust, mapMaybe, catMaybes) import Data.List (intersperse, intercalate, partition) import Control.Arrow (second) import System.FilePath.Posix ((), (<.>)) @@ -151,8 +151,8 @@ renderPackageFlags render docURL = code = (thespan ! [theclass "code"] <<) whenNotNull xs a = if null xs then [] else a -moduleSection :: PackageRender -> Maybe TarIndex -> URL -> Bool -> [Html] -moduleSection render mdocIndex docURL quickNav = +moduleSection :: PackageRender -> Maybe TarIndex -> URL -> Maybe PackageId -> Bool -> [Html] +moduleSection render mdocIndex docURL mPkgId quickNav = maybeToList $ fmap msect (rendModules render mdocIndex) where msect ModSigIndex{ modIndex = m, sigIndex = s } = toHtml $ (if not (null s) @@ -164,16 +164,25 @@ moduleSection render mdocIndex docURL quickNav = [renderDocIndexLink] ++ [renderModuleForest docURL m ] else []) - renderDocIndexLink = case mdocIndex of - Just tindex -> - let docIndexURL | isJust (Tar.lookup tindex "doc-index-All.html") = docURL "doc-index-All.html" - | otherwise = docURL "doc-index.html" - in paragraph ! [thestyle "font-size: small"] - << ("[" +++ anchor ! [href docIndexURL] << "Index" +++ "]" +++ - (if quickNav - then " [" +++ anchor ! [identifier "quickjump-trigger", href "#"] << "Quick Jump" +++ "]" - else mempty)) - Nothing -> mempty + renderDocIndexLink = case concatLinks indexLinks of + Nothing -> mempty + Just links -> paragraph ! [thestyle "font-size: small"] << ("[" +++ links +++ "]") + where + indexLinks = catMaybes $ case mdocIndex of + Just tindex -> + let docIndexURL | isJust (Tar.lookup tindex "doc-index-All.html") = docURL "doc-index-All.html" + | otherwise = docURL "doc-index.html" + in [ Just $ anchor ! [href docIndexURL] << "Index" + , if quickNav + then Just $ anchor ! [identifier "quickjump-trigger", href "#"] << "Quick Jump" + else Nothing + ] + Nothing -> [] + ++ [fmap (\pkgId -> anchor ! [href (packageURL pkgId)] << "Last Documentation") mPkgId] + + concatLinks [] = Nothing + concatLinks [h] = Just h + concatLinks (h:hs) = (h +++) . ("] [" +++) <$> concatLinks hs tabulate :: [(String, Html)] -> Html tabulate items = table ! [theclass "properties"] << diff --git a/src/Distribution/Server/Pages/Package/HaddockHtml.hs b/src/Distribution/Server/Pages/Package/HaddockHtml.hs index dba250b9f..8b5b3f0d5 100644 --- a/src/Distribution/Server/Pages/Package/HaddockHtml.hs +++ b/src/Distribution/Server/Pages/Package/HaddockHtml.hs @@ -24,7 +24,7 @@ htmlMarkup modResolv = Markup { markupBold = strong, markupMonospaced = thecode, markupUnorderedList = unordList, - markupOrderedList = ordList, + markupOrderedList = ordList . map snd, markupDefList = defList, markupCodeBlock = pre, markupHyperlink = \(Hyperlink url mLabel) -> anchor ! [href url] << maybe url showHtmlFragment mLabel, diff --git a/src/Distribution/Server/Pages/PackageFromTemplate.hs b/src/Distribution/Server/Pages/PackageFromTemplate.hs index d54883fa2..6cf3e2624 100644 --- a/src/Distribution/Server/Pages/PackageFromTemplate.hs +++ b/src/Distribution/Server/Pages/PackageFromTemplate.hs @@ -80,14 +80,14 @@ import Distribution.Server.Features.Html.HtmlUtilities -- votes it has. packagePageTemplate :: PackageRender -> Maybe TarIndex -> Maybe DocMeta -> Maybe BS.ByteString - -> URL -> [(DistroName, DistroPackageInfo)] + -> URL -> Maybe PackageId -> [(DistroName, DistroPackageInfo)] -> Maybe [PackageName] -> HtmlUtilities -> Bool -> [TemplateAttr] packagePageTemplate render mdocIndex mdocMeta mreadme - docURL distributions + docURL mPkgId distributions deprs utilities isCandidate = if isCandidate then @@ -97,7 +97,7 @@ packagePageTemplate render , "doc" $= docFieldsTemplate ] ++ -- Miscellaneous things that could still stand to be refactored a bit. - [ "moduleList" $= Old.moduleSection render mdocIndex docURL False + [ "moduleList" $= Old.moduleSection render mdocIndex docURL mPkgId hasQuickNav , "downloadSection" $= Old.downloadSection render ] else @@ -107,7 +107,7 @@ packagePageTemplate render , "doc" $= docFieldsTemplate ] ++ -- Miscellaneous things that could still stand to be refactored a bit. - [ "moduleList" $= Old.moduleSection render mdocIndex docURL hasQuickNav + [ "moduleList" $= Old.moduleSection render mdocIndex docURL mPkgId hasQuickNav , "executables" $= (commaList . map toHtml $ rendExecNames render) , "downloadSection" $= Old.downloadSection render , "stability" $= renderStability desc @@ -166,19 +166,15 @@ packagePageTemplate render , templateVal "license" (Old.rendLicense render) , templateVal "author" (toHtml $ author desc) , templateVal "maintainer" (Old.maintainField $ rendMaintainer render) - , templateVal "maintainerURL" (toHtml $ - anchor ! [href $ "/package" pkgName "maintainers" ] << "package maintainers") + , templateVal "maintainerURL" (toHtml $ "/package" pkgName "maintainers") , templateVal "buildDepends" (snd (Old.renderDependencies render)) , templateVal "optional" optionalPackageInfoTemplate , templateVal "candidateBanner" candidateBanner ] docFieldsTemplate = - if isCandidate - then templateDict [ templateVal "baseUrl" docURL ] - else templateDict [ templateVal "hasQuickNavV1" hasQuickNavV1 - , templateVal "baseUrl" docURL - ] + templateDict [ templateVal "hasQuickNavV1" hasQuickNavV1 + , templateVal "baseUrl" docURL ] -- Fields that may be empty, along with booleans to see if they're present. -- Access via "$package.optional.varname$" @@ -340,7 +336,6 @@ candidatesPageTemplate cands candidates candidatesCore= , toHtml $ ". " ++ fromShortText (synopsis desc) ] - -- #ToDo: Pick out several interesting versions to display, with a link to -- display all versions. renderVersion :: PackageId -> [(Version, VersionStatus)] -> Maybe String -> Html diff --git a/src/Distribution/Server/Pages/Recent.hs b/src/Distribution/Server/Pages/Recent.hs index 6f400373e..65b81eac7 100644 --- a/src/Distribution/Server/Pages/Recent.hs +++ b/src/Distribution/Server/Pages/Recent.hs @@ -1,10 +1,11 @@ -- Takes a reversed log file on the standard input and outputs web page. +{-# LANGUAGE NamedFieldPuns #-} module Distribution.Server.Pages.Recent ( recentPage, recentFeed, revisionsPage, - recentRevisionsFeed + recentRevisionsFeed, ) where import Distribution.Server.Packages.Types @@ -23,43 +24,109 @@ import Distribution.Text import Distribution.Utils.ShortText (fromShortText) import qualified Text.XHtml.Strict as XHtml -import Text.XHtml - ( Html, URL, (<<), (!) ) +import Text.XHtml ( Html, URL, (<<), (!) ) import qualified Text.RSS as RSS -import Text.RSS - ( RSS(RSS) ) -import Network.URI - ( URI(..), uriToString ) -import Data.Time.Clock - ( UTCTime, addUTCTime ) -import Data.Time.Format - ( defaultTimeLocale, formatTime ) -import Data.Maybe - ( listToMaybe) +import Text.RSS ( RSS(RSS) ) +import Network.URI ( URI(..), uriToString ) +import Data.Time.Clock ( UTCTime ) +import Data.Time.Format ( defaultTimeLocale, formatTime ) +import Data.Maybe ( listToMaybe, fromMaybe) +import Distribution.Server.Util.Paging (PaginatedConfiguration(..), hasNext, + hasPrev, nextURL, pageIndexRange, paginate, prevURL, toURL, allPagedURLs, pagingInfo) -- | Takes a list of package info, in reverse order by timestamp. --- -recentPage :: Users -> [PkgInfo] -> Html -recentPage users pkgs = - let log_rows = map (makeRow users) (take 25 pkgs) - docBody = [XHtml.h2 << "Recent additions", - XHtml.table ! [XHtml.align "center"] << log_rows, - XHtml.anchor ! [XHtml.href recentRevisionsURL] << XHtml.toHtml "Recent revisions"] - rss_link = XHtml.thelink ! [XHtml.rel "alternate", - XHtml.thetype "application/rss+xml", - XHtml.title "Hackage RSS Feed", - XHtml.href rssFeedURL] << XHtml.noHtml + +recentPage :: PaginatedConfiguration -> Users -> [PkgInfo] -> Html +recentPage conf users pkgs = + let log_rows = makeRow users <$> paginate conf pkgs + docBody = + [ XHtml.h2 << "Recent additions", + pageSizeForm recentURL, + XHtml.table ! [XHtml.align "center"] << log_rows, + paginator conf recentURL, + XHtml.anchor ! [XHtml.href recentRevisionsURL] << XHtml.toHtml "Recent revisions" + ] + rss_link = + XHtml.thelink + ! [ XHtml.rel "alternate", + XHtml.thetype "application/rss+xml", + XHtml.title "Hackage RSS Feed", + XHtml.href $ toURL rssFeedURL conf + ] + << XHtml.noHtml in hackagePageWithHead [rss_link] "recent additions" docBody -revisionsPage :: Users -> [PkgInfo] -> Html -revisionsPage users pkgs = - let log_rows = map (makeRevisionRow users) (take 40 pkgs) - docBody = [XHtml.h2 << "Recent cabal metadata revisions", - XHtml.table ! [XHtml.align "center"] << log_rows] - rss_link = XHtml.thelink ! [XHtml.rel "alternate", - XHtml.thetype "application/rss+xml", - XHtml.title "Hackage Revisions RSS Feed", - XHtml.href revisionsRssFeedURL] << XHtml.noHtml + +pageSizeForm :: URL -> Html +pageSizeForm base = + let pageSizeLabel = XHtml.label ! [XHtml.thefor "pageSize"] << "Page Size: " + pageSizeInput = XHtml.input ! [XHtml.thetype "number", XHtml.name "pageSize", XHtml.strAttr "min" "0"] + submitButton = XHtml.button ! [XHtml.thetype "submit"] << "Submit" + theForm = XHtml.form ! [XHtml.action base, XHtml.method "GET"] + in theForm << (pageSizeLabel <> pageSizeInput <> submitButton) + + +paginator :: PaginatedConfiguration -> URL -> Html +paginator pc@PaginatedConfiguration{currPage} baseUrl = + let + info = XHtml.thediv << pagingInfo pc + + next = XHtml.anchor ! [XHtml.href (fromMaybe "" (nextURL baseUrl pc)) | hasNext pc] << "Next" + prev = XHtml.anchor ! [XHtml.href (fromMaybe "" (prevURL baseUrl pc)) | hasPrev pc] << "Previous" + + + pagedURLS = zip [1..] (allPagedURLs baseUrl pc) + pagedLinks = (\(x,y) -> XHtml.anchor ! [XHtml.href y, + if currPage == x then XHtml.theclass "current" else noAttr ] << show x) <$> pagedURLS + + wrapper = XHtml.thediv ! [XHtml.theclass "paginator"] << + (prev <> reducePagedLinks pc pagedLinks <> next) + + + in XHtml.thediv ! [XHtml.identifier "paginatorContainer"] << mconcat [info, wrapper] + +noAttr :: XHtml.HtmlAttr +noAttr = XHtml.theclass "" + +-- | Generates a list of links of the current possible paging links, recreates the functionality of the paging links on the search page +reducePagedLinks :: PaginatedConfiguration -> [Html] -> Html +reducePagedLinks PaginatedConfiguration{currPage} xs + | length xs <= 5 = mconcat xs -- Do Nothing + | currPage >= (length xs - 3) = mconcat . keepLastPages .fillFirst $ xs -- Beginning ellipses + | currPage < 5 = mconcat . keepFirstPages . fillLast $ xs -- Ending ellipses + | otherwise = mconcat . keepMiddlePages . fillLast . fillFirst $ xs -- Begin and End ellipses + where filler = XHtml.thespan << "..." + fillFirst x = insertAt 1 filler x + fillLast x = insertAt (pred . length $ x) filler x + keepFirstPages x = case splitAt (length x - 2) x of (hts, hts') -> take 5 hts ++ hts' + keepLastPages x = case splitAt 2 x of (hts, hts') -> hts ++ takeLast 5 hts' + keepMiddlePages x = + case splitAt currPage x of (hts, hts') -> take 2 hts ++ [last hts] ++ take 2 hts' + ++ takeLast 2 hts' + +insertAt :: Int -> a -> [a] -> [a] +insertAt n a x = case splitAt n x of (hts, hts') -> hts ++ [a] ++ hts' + +takeLast :: Int -> [a] -> [a] +takeLast n = reverse . take n . reverse + +revisionsPage :: PaginatedConfiguration -> Users -> [PkgInfo] -> Html +revisionsPage conf users pkgs = + let log_rows = map (makeRevisionRow users) (paginate conf pkgs) + docBody = + [ XHtml.h2 << "Recent cabal metadata revisions", + pageSizeForm recentRevisionsURL, + XHtml.table ! [XHtml.align "center"] << log_rows, + paginator conf recentRevisionsURL + ] + rss_link = + XHtml.thelink + ! [ XHtml.rel "alternate", + XHtml.thetype "application/rss+xml", + XHtml.title "Hackage Revisions RSS Feed", + XHtml.href $ toURL revisionsRssFeedURL conf + ] + << XHtml.noHtml in hackagePageWithHead [rss_link] "recent revisions" docBody makeRow :: Users -> PkgInfo -> Html @@ -110,6 +177,9 @@ packageURL pkgid = "/package/" ++ display pkgid rssFeedURL :: URL rssFeedURL = "/recent.rss" +recentURL :: URL +recentURL = "/packages/recent.html" + recentAdditionsURL :: URL recentAdditionsURL = "/recent.html" @@ -120,36 +190,34 @@ recentRevisionsURL :: URL recentRevisionsURL = "/packages/recent/revisions.html" -recentFeed :: Users -> URI -> UTCTime -> [PkgInfo] -> RSS -recentFeed users hostURI now pkgs = RSS +recentFeed :: PaginatedConfiguration -> Users -> URI -> UTCTime -> [PkgInfo] -> RSS +recentFeed conf users hostURI now pkgs = RSS "Recent additions" (hostURI { uriPath = recentAdditionsURL}) desc (channel updated) (map (releaseItem users hostURI) pkgList) where - desc = "The 20 most recent additions to Hackage (or last 48 hours worth, whichever is greater), the Haskell package database." - twoDaysAgo = addUTCTime (negate $ 60 * 60 * 48) now - pkgListTwoDays = takeWhile (\p -> pkgLatestUploadTime p > twoDaysAgo) pkgs - pkgList = if length pkgListTwoDays > 20 then pkgListTwoDays else take 20 pkgs + (start,end) = pageIndexRange conf + desc = "Showing " ++ show start ++ " - " ++ show end ++ " most recent additions to Hackage, the Haskell package database." + pkgList = paginate conf pkgs updated = maybe now (fst . pkgOriginalUploadInfo) (listToMaybe pkgList) -recentRevisionsFeed :: Users -> URI -> UTCTime -> [PkgInfo] -> RSS -recentRevisionsFeed users hostURI now pkgs = RSS +recentRevisionsFeed :: PaginatedConfiguration -> Users -> URI -> UTCTime -> [PkgInfo] -> RSS +recentRevisionsFeed conf users hostURI now pkgs = RSS "Recent revisions" (hostURI { uriPath = recentRevisionsURL}) desc (channel updated) (map (revisionItem users hostURI) pkgList) where - desc = "The 40 most recent revisions to cabal metadata in Hackage (or last 48 hours worth, whichever is greater), the Haskell package database." - twoDaysAgo = addUTCTime (negate $ 60 * 60 * 48) now - pkgListTwoDays = takeWhile (\p -> pkgLatestUploadTime p > twoDaysAgo) pkgs - pkgList = if length pkgListTwoDays > 40 then pkgListTwoDays else take 40 pkgs + (start, end) = pageIndexRange conf + desc = "Showing " ++ show start ++ " - " ++ show end ++ " most recent revisions to cabal metadata in Hackage, the Haskell package database." + pkgList = paginate conf pkgs updated = maybe now (fst . pkgOriginalUploadInfo) (listToMaybe pkgList) channel :: UTCTime -> [RSS.ChannelElem] -channel updated = +channel updated = [ RSS.Language "en" , RSS.ManagingEditor email , RSS.WebMaster email diff --git a/src/Distribution/Server/Pages/Reverse.hs b/src/Distribution/Server/Pages/Reverse.hs index 69a371c0a..aa8a288aa 100644 --- a/src/Distribution/Server/Pages/Reverse.hs +++ b/src/Distribution/Server/Pages/Reverse.hs @@ -1,177 +1,141 @@ +{-# LANGUAGE NamedFieldPuns, RecordWildCards, BlockArguments #-} module Distribution.Server.Pages.Reverse ( - reversePackageRender, - reverseFlatRender, - reverseStatsRender, - reversePackagesRender, - reversePackageSummary + ReverseHtmlUtil(..) + , reverseHtmlUtil + , LatestOrOld(..) ) where import Distribution.Server.Features.ReverseDependencies -import Distribution.Server.Packages.Reverse -import Distribution.Server.Packages.Preferred +import Distribution.Server.Features.PreferredVersions + import Distribution.Package import Distribution.Text (display) import Distribution.Version +import Data.Function ((&)) import qualified Data.Map as Map +import Data.Set (Set, toList) import Text.XHtml.Strict -reversePackageRender :: PackageId -- ^ The package whose information is displayed. - -> (PackageId -> String) -- ^ Generating URIs for package pages. - -> ReverseResource -- ^ The resource for generating revdeps-related URIs. - -> Bool -- ^ Whether the ReverseDisplay was rendered for recent (True) or older (False) versions of the package. - -> ReversePageRender -- ^ Obtained from a ReverseDisplay-rendering function. - -> [Html] -reversePackageRender pkgid packageLink r isRecent (ReversePageRender renders counts total) = - let packageAnchor = anchor ! [href $ packageLink pkgid] << display pkgid - hasVersion = packageVersion pkgid /= Version [] [] - pkgname = packageName pkgid - statLinks = paragraph << - [ toHtml "Check out the " - , anchor ! [href $ reverseStatsUri r "" pkgname] << "statistics for specific versions" - , toHtml $ " of " ++ display pkgid ++ " and its " - , anchor ! [href $ reverseAllUri r "" pkgname] << "indirect dependencies", toHtml "." ] - versionBox = if hasVersion && total /= allCounts - then thediv ! [theclass "notification"] << [toHtml $ "These statistics only apply to this version of " ++ display pkgname ++ ". See also ", anchor ! [href $ reverseNameUri r "" pkgname] << [toHtml "packages which depend on ", emphasize << "any", toHtml " version"], toHtml $ " (all " ++ show total ++ " of them)."] - else noHtml - allCounts = uncurry (+) counts - otherCount = case total - allCounts of - diff | diff > 0 -> paragraph << [show diff ++ " packages depend on versions of " ++ display pkgid ++ " other than this one."] - _ -> noHtml - (pageText, nonPageText) = (if isRecent then id else uncurry $ flip (,)) (recentText, nonRecentText) - otherLink = if isRecent then reverseOldUri r "" pkgid else reverseUri r "" pkgid - in h2 << (display pkgid ++ ": " ++ num allCounts "reverse dependencies" "reverse dependency"):versionBox:case counts of - (0, 0) -> - [ paragraph << [toHtml "No packages depend on ", - packageAnchor, toHtml "."] - ] - (0, count) -> - [ paragraph << [toHtml "No packages depend on ", - if hasVersion then noHtml else toHtml "some version of ", - packageAnchor, - toHtml $ pageText 0 ++ " However, ", - altVersions count nonPageText otherLink, - toHtml "."] - ] ++ [otherCount, statLinks] - (count, count') -> - [ (paragraph<<) $ [ mainVersions count pageText packageAnchor, toHtml " (listed below)." ] - ++ if count' > 0 then [ toHtml " Additionally, " - , altVersions count' nonPageText otherLink - , toHtml $ ". That's " ++ show (count+count') ++ " in total." ] - else [] - ] ++ (if isRecent then [] else [paragraph << oldText]) ++ [otherCount, statLinks, reverseTable] - where - mainVersions count textFunc pkgLink = toHtml - [ toHtml $ num count "packages depend on " "package depends on " - , pkgLink - , toHtml $ textFunc count - ] - altVersions count textFunc altLink = toHtml - [ anchor ! [href altLink] << num count "packages" "package" - , toHtml $ num' count " depend on " " depends on " ++ display pkgid ++ textFunc count - ] - recentText count = ' ':num' count "in their latest versions" "in its latest version" - nonRecentText count = ' ':num' count "only in older or deprecated versions" "only in an older or deprecated version" - oldText = "The latest version of each package below, which doesn't depend on " ++ display pkgid ++ ", is linked from the first column. The version linked from the second column is the one which has a dependency on " ++ display pkgid ++", but it's no longer the preferred installation candidate. Note that packages which depend on versions of " ++ display pkgid ++ " not uploaded to Hackage are treated as not depending on it at all." - - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Version", th << "Reverse dependencies" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink $ PackageIdentifier (packageName pkg) $ Version [] [] ] << display (packageName pkg) - , td << anchor ! (renderStatus status ++ [href $ packageLink pkg]) << display (packageVersion pkg) - , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseNameUri r "" $ packageName pkg] << "view", toHtml ")" ] ] - | (ReverseRender pkg status count, n) <- zip renders [(1::Int)..] ] - - renderStatus (Just DeprecatedVersion) = [theclass "deprecated"] - renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] - renderStatus _ = [] - -reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseResource -> ReverseCount -> [(PackageName, Int)] -> [Html] -reverseFlatRender pkgname packageLink r (ReverseCount total flat _) pairs = - h2 << (display pkgname ++ ": " ++ num flat "total reverse dependencies" "reverse dependency"):case (total, flat) of - (0, 0) -> [paragraph << [toHtml "No packages depend on ", toPackage pkgname]] - _ -> - [ paragraph << if total == flat - then [toHtml "All packages which use ", toPackage pkgname, toHtml " depend on it ", anchor ! [href $ reverseNameUri r "" pkgname] << "directly", toHtml $ ". " ++ onlyPackage total] - else [toPackage pkgname, toHtml " has ", anchor ! [href $ reverseNameUri r "" pkgname] << num total "packages" "package", toHtml $ " which directly " ++ num' total "depend" "depends" ++ " on it, but there are more packages which depend on ", emphasize << "those", toHtml $ " packages. If you flatten the tree of reverse dependencies, you'll find " ++ show flat ++ " packages which use " ++ display pkgname ++ ", and " ++ show (flat-total) ++ " which do so without depending directly on it. All of these packages are listed below."] - , paragraph << [toHtml "See also the ", anchor ! [href $ reverseStatsUri r "" pkgname] << "statistics for specific versions", toHtml $ " of " ++ display pkgname ++ "."] - , reverseTable - ] - where - toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg +data LatestOrOld + = OnlyLatest + | OnlyOlder + deriving Eq - onlyPackage count = if count == 1 then "There's only one:" else "There are " ++ show count ++ ":" +data ReverseHtmlUtil = ReverseHtmlUtil { + reversePackageRender :: PackageId -> (PackageId -> String) -> LatestOrOld -> ReversePageRender -> [Html] + , reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseCount -> [(PackageName, Int)] -> [Html] + , reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] + , reversePackagesRender :: (PackageName -> String) -> Int -> [(PackageName, ReverseCount)] -> [Html] + } - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Total reverse dependencies" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << toPackage pkg - , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseAllUri r "" pkg] << "view", toHtml ")" ] - ] - | ((pkg, count), n) <- zip pairs [(1::Int)..] ] - --- /package/:package/reverse/summary -reverseStatsRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseResource -> ReverseCount -> [Html] -reverseStatsRender pkgname allVersions packageLink r (ReverseCount total flat versions) = - h2 << (display pkgname ++ ": reverse dependency statistics"): - [ case total of - 0 -> paragraph << [ toHtml "No packages depend on ", thisPackage, toHtml "." ] - _ -> toHtml - [ paragraph << [ anchor ! [href $ reverseNameUri r "" pkgname] << num total "packages" "package" - , toHtml $ num' total " depend" " depends" - , toHtml " directly on ", thisPackage, toHtml "." ] - , paragraph << [ toHtml $ num (flat-total) "packages depend" "package depends" ++ " indirectly on " ++ display pkgname ++ "." ] - , paragraph << [ anchor ! [href $ reverseAllUri r "" pkgname] << num flat "packages" "package" - , toHtml $ num' flat " depend" " depends" ++ " on " ++ display pkgname ++ " in total." - ] - ] - , versionTable ] +reverseHtmlUtil :: ReverseFeature -> ReverseHtmlUtil +reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} where - toPackage pkgid = anchor ! [href $ packageLink pkgid] << display pkgid - thisPackage = toPackage (PackageIdentifier pkgname $ Version [] []) - - versionTable = thediv << table << versionTableRows - versionTableRows = - [ tr << [ th << "Version", th << "Reverse dependency count" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink pkgid ] << display version - , td << [ toHtml $ show (Map.findWithDefault 0 version versions) ++ " (" - , anchor ! [href $ reverseUri r "" pkgid] << "view", toHtml ")" ] + reversePackageRender :: PackageId -- ^ The package whose information is displayed. + -> (PackageId -> String) -- ^ Generating URIs for package pages. + -> LatestOrOld -- ^ Whether the ReverseDisplay was rendered for recent (OnlyLatest) or older (OnlyOlder) versions of the package. + -> ReversePageRender -- ^ Obtained from a ReverseDisplay-rendering function. + -> [Html] + reversePackageRender pkgid packageLink isRecent (ReversePageRender renders (count, count') total) = + let allCounts = count + count' + firstTh = toHtml ("Depend on the " <> (if packageVersion pkgid == nullVersion then "latest" else "given") <> " version") + in h2 << (display pkgid ++ ": " ++ display allCounts ++ " reverse dependencies"): + [ if isRecent == OnlyLatest + then toHtml "No version specified, so showing reverse dependencies for latest version." + else toHtml "" + , table ! [ theclass "fancy" ] + << [ tr << [ th << firstTh, th << toHtml "Depend on other versions", th << toHtml "Total" ] + , tr << [ td << toHtml (display count), td << toHtml (display count'), td << toHtml (display total) ]] + , reverseTable + ] + where + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + tr ! [theclass "fancy"] << [ th << "Package name", th << "Version", th << "Reverse dependencies" ] : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink $ PackageIdentifier (packageName pkg) $ nullVersion ] << display (packageName pkg) + , td << anchor ! (renderStatus status ++ [href $ packageLink pkg]) << display (packageVersion pkg) + , td << [ toHtml $ (show count'') ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" $ packageName pkg] << "view", toHtml ")" ] ] + | (ReverseRender pkg status count'', n) <- zip renders [(1::Int)..] ] + + renderStatus (Just DeprecatedVersion) = [theclass "deprecated"] + renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] + renderStatus _ = [] + + renderCount ReverseCount{totalCount, directCount} = + table ! [ theclass "fancy" ] + << [ tr << [ th << firstTh, th << secondTh, th << toHtml "Total" ] + , tr << [ td << toHtml (display directCount), td << toHtml (display (totalCount - directCount)), td << toHtml (display totalCount) ]] + where + firstTh = toHtml "Direct reverse dependencies" + secondTh = toHtml "Indirect reverse dependencies" + + reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseCount -> [(PackageName, Int)] -> [Html] + reverseFlatRender pkgname packageLink revCount pairs = + h2 << (display pkgname ++ ": " ++ "total reverse dependencies"):renderCount revCount:[reverseTable] + where + + toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg + + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + (tr ! [theclass "fancy"] << [ th << "Package name", th << "Total reverse dependencies" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << toPackage pkg + , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" pkg] << "view", toHtml ")" ] + ] + | ((pkg, count), n) <- zip pairs [(1::Int)..] ] + + -- /package/:package/reverse/verbose + reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] + reverseVerboseRender pkgname allVersions packageLink revCount versions = + h2 << (display pkgname ++ ": reverse dependencies per version"): + [ renderCount revCount + , versionTable + ] + + where + versionTable = thediv << (table ! [theclass "fancy"]) << versionTableRows + versionTableRows = + (tr << [ th << "Version", th << "Reverse dependencies" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink pkgid ] << display version + , td << [ row ] + ] + | (version, n) <- zip allVersions [(1::Int)..] + , let + pkgid = PackageIdentifier pkgname version + mkListOfLinks :: Set PackageIdentifier -> [Html] + mkListOfLinks pkgIdSet = + toList pkgIdSet & + map + \revDepPkgId -> + li << + anchor ! [href $ packageLink revDepPkgId] << display revDepPkgId + row :: Html + row = + Map.lookup version versions & + maybe + (toHtml "This version has no reverse dependencies.") + ((ulist <<) . mkListOfLinks) ] - | (version, n) <- zip allVersions [(1::Int)..], let pkgid = PackageIdentifier pkgname version ] - -num, num' :: Int -> String -> String -> String -num n plural singular = show n ++ " " ++ num' n plural singular -num' n plural singular = if n == 1 then singular else plural - --- /packages/reverse -reversePackagesRender :: (PackageName -> String) -> ReverseResource -> Int -> [(PackageName, Int, Int)] -> [Html] -reversePackagesRender packageLink r pkgCount triples = - h2 << "Reverse dependencies" : - [ paragraph << [ "Hackage has " ++ show pkgCount ++ " packages. Here are all the packages that have package that depend on them:"] - , reverseTable ] - where - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Total", th << "Direct" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink pkgname ] << display pkgname - , td << [ toHtml $ show flat ++ " (", anchor ! [href $ reverseStatsUri r "" pkgname ] << "view", toHtml ")" ] - , td << [ toHtml $ show total ++ " (", anchor ! [href $ reverseNameUri r "" pkgname ] << "view", toHtml ")" ] - ] -- and, indirect is flat-total, if those are ever explicitly served - | ((pkgname, total, flat), n) <- zip triples [(1::Int)..], flat /= 0 ] - -reversePackageSummary :: PackageId -> ReverseResource -> (Int, Int) -> (String, Html) -reversePackageSummary pkgid r (direct, version) = (,) "Reverse dependencies" $ - if direct == 0 - then toHtml "None" - else toHtml [ anchor ! [href $ reverseUri r "" pkgid ] << show version - , toHtml " for ", toHtml . display $ packageVersion pkgid - , toHtml " and " - , anchor ! [href $ reverseNameUri r "" (packageName pkgid) ] << show direct - , toHtml " total"] + -- /packages/reverse + reversePackagesRender :: (PackageName -> String) -> Int -> [(PackageName, ReverseCount)] -> [Html] + reversePackagesRender packageLink pkgCount namesWithCounts = + h2 << "Reverse dependencies" : + [ paragraph << [ "Hackage has " ++ show pkgCount ++ " packages. Here are all the packages that have package that depend on them:"] + , reverseTable ] + where + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + (tr ! [theclass "fancy"] << [ th << "Package name", th << "Total", th << "Direct" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink pkgname ] << display pkgname + , td << [ toHtml $ show totalCount ++ " (", anchor ! [href $ reverseVerboseUri reverseResource "" pkgname ] << "view", toHtml ")" ] + , td << [ toHtml $ show directCount ++ " (", anchor ! [href $ reverseNameUri reverseResource "" pkgname ] << "view", toHtml ")" ] + ] -- and, indirect is total-direct, if those are ever explicitly served + | ((pkgname, ReverseCount{directCount, totalCount}), n) <- zip namesWithCounts [(1::Int)..], totalCount /= 0 ] diff --git a/src/Distribution/Server/Util/CabalRevisions.hs b/src/Distribution/Server/Util/CabalRevisions.hs index f417dee1e..d984c4cc3 100644 --- a/src/Distribution/Server/Util/CabalRevisions.hs +++ b/src/Distribution/Server/Util/CabalRevisions.hs @@ -2,6 +2,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} @@ -39,6 +40,9 @@ import Distribution.Version import Distribution.Compiler (CompilerFlavor) import Distribution.FieldGrammar (prettyFieldGrammar) import Distribution.Fields.Pretty (PrettyField (..), showFields) +#if MIN_VERSION_Cabal(3,7,0) +import Distribution.Fields.Pretty (pattern NoComment) +#endif import Distribution.PackageDescription import Distribution.PackageDescription.Parsec (parseGenericPackageDescription, runParseResult) import Distribution.PackageDescription.FieldGrammar (sourceRepoFieldGrammar) @@ -340,7 +344,7 @@ checkPackageDescriptions checkXRevision checkSame "The package-url field is unused, don't bother changing it." pkgUrlA pkgUrlB changesOk "bug-reports" fromShortText bugReportsA bugReportsB - changesOkList changesOk "source-repository" (showFields (const []) . (:[]) . ppSourceRepo) + changesOkList changesOk "source-repository" (showFields noComment . (:[]) . ppSourceRepo) sourceReposA sourceReposB changesOk "synopsis" fromShortText synopsisA synopsisB changesOk "description" fromShortText descriptionA descriptionB @@ -365,6 +369,12 @@ checkPackageDescriptions checkXRevision when checkXRevision $ checkRevision customFieldsPDA customFieldsPDB checkCuration customFieldsPDA customFieldsPDB + where +#if MIN_VERSION_Cabal(3,7,0) + noComment _ = NoComment +#else + noComment _ = [] +#endif checkSpecVersionRaw :: Check PackageDescription checkSpecVersionRaw pdA pdB @@ -625,10 +635,20 @@ checkExecutable componentName checkTestSuite :: ComponentName -> Check TestSuite checkTestSuite componentName +#if MIN_VERSION_Cabal(3,7,0) + (TestSuite _nameA interfaceA buildInfoA testGeneratorsA) + (TestSuite _nameB interfaceB buildInfoB testGeneratorsB) +#else (TestSuite _nameA interfaceA buildInfoA) - (TestSuite _nameB interfaceB buildInfoB) = do + (TestSuite _nameB interfaceB buildInfoB) +#endif + = do checkSame "Cannot change test-suite type" interfaceA interfaceB checkBuildInfo componentName buildInfoA buildInfoB +#if MIN_VERSION_Cabal(3,7,0) + -- @test-generators@ + checkSame "Cannot change test-generators" testGeneratorsA testGeneratorsB +#endif checkBenchmark :: ComponentName -> Check Benchmark checkBenchmark componentName @@ -690,6 +710,7 @@ changesOkSet what render old new = do logChange (Change Normal ("removed " ++ what) (renderSet removed) "") unless (Set.null added) $ logChange (Change Normal ("added " ++ what) "" (renderSet added)) + return () where added = new Set.\\ old removed = old Set.\\ new diff --git a/src/Distribution/Server/Util/Markdown.hs b/src/Distribution/Server/Util/Markdown.hs index bc8fb84a6..881298f48 100644 --- a/src/Distribution/Server/Util/Markdown.hs +++ b/src/Distribution/Server/Util/Markdown.hs @@ -22,7 +22,7 @@ import qualified Data.Text.Encoding.Error as T (lenientDecode) import qualified Data.Text.Lazy as TL import Data.Typeable (Typeable) import Network.URI (isRelativeReference) -import Control.Monad.Identity +import Control.Monad.Identity (runIdentity) import Text.HTML.SanitizeXSS as XSS import System.FilePath.Posix (takeExtension) import qualified Data.ByteString.Lazy as BS (ByteString, toStrict) @@ -120,6 +120,30 @@ adjustRelativeLink url --

Published to http://hackage.haskell.org/foo3/bar.

-- -- +-- >>> renderMarkdown "test" "Issue #1105:\n- pipes\n- like `a|b`\n- should be allowed in lists" +--

Issue #1105:

+--
    +--
  • pipes +--
  • +--
  • like a|b +--
  • +--
  • should be allowed in lists +--
  • +--
+-- +-- +-- >>> renderMarkdown "test" "Tables should be supported:\n\nfoo|bar\n---|---\n" +--

Tables should be supported:

+-- +-- +-- +-- +-- +-- +-- +--
foobar
+-- +-- renderMarkdown :: String -- ^ Name or path of input. -> BS.ByteString -- ^ Commonmark text input. @@ -160,11 +184,33 @@ renderMarkdown' -> BS.ByteString -- ^ Commonmark text input. -> XHtml.Html -- ^ Rendered HTML. renderMarkdown' render name md = - either (const $ XHtml.pre XHtml.<< T.unpack txt) (XHtml.primHtml . T.unpack . sanitizeBalance . TL.toStrict . render) $ - runIdentity (commonmarkWith (mathSpec <> gfmExtensions <> defaultSyntaxSpec) - name - txt) - where txt = T.decodeUtf8With T.lenientDecode . BS.toStrict $ md + either (const $ fallback) mdToHTML $ + runIdentity $ commonmarkWith spec name txt + where + -- Input + txt = T.decodeUtf8With T.lenientDecode . BS.toStrict $ md + -- Fall back to HTML if there is a parse error for markdown + fallback = XHtml.pre XHtml.<< T.unpack txt + -- Conversion of parsed md to HTML + mdToHTML = XHtml.primHtml . T.unpack . sanitizeBalance . TL.toStrict . render + -- Specification of the markdown parser. + -- Andreas Abel, 2022-07-21, issue #1105. + -- Workaround for https://github.com/jgm/commonmark-hs/issues/95: + -- Put the table parser last. + spec = mconcat $ + mathSpec : + -- all the gfm extensions except for tables + emojiSpec : + strikethroughSpec : + autolinkSpec : + autoIdentifiersSpec : + taskListSpec : + footnoteSpec : + -- the default syntax + defaultSyntaxSpec : + -- the problematic table parser + pipeTableSpec : + [] -- | Does the file extension suggest that the file is in markdown syntax? supposedToBeMarkdown :: FilePath -> Bool diff --git a/src/Distribution/Server/Util/NLP/LICENSE b/src/Distribution/Server/Util/NLP/LICENSE new file mode 100644 index 000000000..1d79b0629 --- /dev/null +++ b/src/Distribution/Server/Util/NLP/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2012, Dag Odenhall +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/Distribution/Server/Util/NLP/Snowball.hs b/src/Distribution/Server/Util/NLP/Snowball.hs new file mode 100644 index 000000000..5778c7c8b --- /dev/null +++ b/src/Distribution/Server/Util/NLP/Snowball.hs @@ -0,0 +1,81 @@ +module Distribution.Server.Util.NLP.Snowball where + +------------------------------------------------------------------------------- +import Control.Concurrent (MVar, newMVar, withMVar) +import Control.Monad (forM, when) +------------------------------------------------------------------------------- +import Data.ByteString.Char8 (packCStringLen, useAsCString) +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +------------------------------------------------------------------------------- +import Foreign (ForeignPtr, FunPtr, Ptr, newForeignPtr, + nullPtr, withForeignPtr) +import Foreign.C (CInt (..), CString) +------------------------------------------------------------------------------- +import System.IO.Unsafe (unsafePerformIO) +------------------------------------------------------------------------------- + +stem :: Text -> Text +stem word = let [a] = stems [word] in a + +stems :: [Text] -> [Text] +stems ws = + unsafePerformIO $ + do stemmer <- newStemmer + stemsWith stemmer ws + +------------------------------------------------------------------------------- + +-- | A thread and memory safe Snowball stemmer instance. +newtype Stemmer = Stemmer (MVar (ForeignPtr Struct)) + +-- | Create a new reusable 'Stemmer' instance. +newStemmer :: IO Stemmer +newStemmer = do + struct <- stemmer_new + when (struct == nullPtr) $ + error "Text.Snowball.newStemmer: nullPtr" + structPtr <- newForeignPtr stemmer_delete struct + mvar <- newMVar (structPtr) + return $ Stemmer mvar + +-- | Use a 'Stemmer' to stem a word. This can be used more efficiently +-- than 'stem' because you can keep a stemmer around and reuse it, but it +-- requires 'IO' to ensure thread safety. +stemWith :: Stemmer -> Text -> IO Text +stemWith stemmer word = do + [a] <- stemsWith stemmer [word] + return a + +-- | Use a 'Stemmer' to stem multiple words in one go. This can be more +-- efficient than @'mapM' 'stemWith'@ because the 'Stemmer' is only +-- locked once. +stemsWith :: Stemmer -> [Text] -> IO [Text] +stemsWith (Stemmer mvar) ws = + withMVar mvar $ \(structPtr) -> + withForeignPtr structPtr $ \struct -> + forM ws $ \word -> + useAsCString (Text.encodeUtf8 word) $ \word' -> + do ptr <- stemmer_stem struct word' $ + fromIntegral $ Text.length word + len <- stemmer_length struct + bytes <- packCStringLen (ptr,fromIntegral len) + return $ Text.decodeUtf8 bytes + + +------------------------------------------------------------------------------- + +data Struct + +foreign import ccall unsafe "libstemmer.h english_ISO_8859_1_stemmer_new" + stemmer_new :: IO (Ptr Struct) + +foreign import ccall unsafe "libstemmer.h &english_ISO_8859_1_stemmer_delete" + stemmer_delete :: FunPtr (Ptr Struct -> IO ()) + +foreign import ccall unsafe "libstemmer.h english_ISO_8859_1_stemmer_stem" + stemmer_stem :: Ptr Struct -> CString -> CInt -> IO (CString) + +foreign import ccall unsafe "libstemmer.h english_ISO_8859_1_stemmer_length" + stemmer_length :: Ptr Struct -> IO CInt diff --git a/src/Distribution/Server/Util/Paging.hs b/src/Distribution/Server/Util/Paging.hs new file mode 100644 index 000000000..07d83cab8 --- /dev/null +++ b/src/Distribution/Server/Util/Paging.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Distribution.Server.Util.Paging +( + totalPages, + createConf, + hasNext, + hasPrev, + paginate, + pageIndexStart, + pageIndexRange, + pageIndexEnd, + allPagedURLs, + nextURL, + prevURL, + toURL, + pagingInfo, + PaginatedConfiguration(..), +) +where +import Text.XHtml (URL) +import Data.List (genericTake, genericDrop, genericLength) + +-- This could be better designed, perhaps turning PaginatedConfiguration into a +-- function that returns the paging info and the paged data +data PaginatedConfiguration = PaginatedConfiguration + { currPage :: Int, + pageSize :: Int, + totalAmount :: Int + } + +-- Assumes pageSize isn't 0, not the best design +totalPages :: PaginatedConfiguration -> Int +totalPages PaginatedConfiguration {pageSize, totalAmount} = + case totalAmount `quotRem` pageSize of + (x,r) + | r == 0 -> x + | otherwise -> succ x + +createConf :: Int -> Int -> [a] -> PaginatedConfiguration +createConf page pageSize xs = PaginatedConfiguration page pageSize (genericLength xs) + +paginate :: PaginatedConfiguration -> [a] -> [a] +paginate PaginatedConfiguration {currPage, pageSize} = genericTake pageSize . genericDrop toDrop + where + toDrop = pageSize * pred currPage + +hasNext,hasPrev :: PaginatedConfiguration -> Bool +hasNext pc@PaginatedConfiguration{currPage} = currPage < totalPages pc +hasPrev PaginatedConfiguration {currPage} = currPage > 1 + +-- | Returns the index positions that the current PaginatedConfiguration would show (Starts at 1) +pageIndexRange :: PaginatedConfiguration -> (Int, Int) +pageIndexRange conf@PaginatedConfiguration{currPage, pageSize, totalAmount} = (start, end) + where start = succ $ currPage * pageSize - pageSize + end = if currPage == totalPages conf then totalAmount else currPage * pageSize + +pageIndexStart, pageIndexEnd :: PaginatedConfiguration -> Int +pageIndexStart = fst . pageIndexRange +pageIndexEnd = snd . pageIndexRange + + +allPagedURLs :: URL -> PaginatedConfiguration -> [URL] +allPagedURLs base pc = toURL base . (\x -> pc{currPage=x}) <$> [1..totalPages pc] + + +-- | Converts the PaginatedConfiguration to a URL, Assumes no query params in url +toURL :: URL -> PaginatedConfiguration -> URL +toURL base PaginatedConfiguration{currPage, pageSize} = base ++ "?page=" ++ show currPage ++ "&pageSize=" ++ show pageSize + +nextURL :: URL -> PaginatedConfiguration -> Maybe URL +nextURL base conf@PaginatedConfiguration {currPage} + | page > totalPages conf = Nothing + | otherwise = Just $ toURL base conf{currPage = page} + where page = succ currPage + +prevURL :: URL -> PaginatedConfiguration -> Maybe URL +prevURL base conf@PaginatedConfiguration {currPage} + | page < 1 = Nothing + | otherwise = Just $ toURL base conf{currPage=page} + where page = pred currPage + + +pagingInfo :: PaginatedConfiguration -> String +pagingInfo pc@PaginatedConfiguration{totalAmount} = "Showing " ++ show start ++ " to " + ++ show end ++ " of " ++ show totalAmount ++ endingText + where (start, end) = pageIndexRange pc + endingText = if pageAmount > 0 then " entries" else " entry" + pageAmount = end - start -- Starts Indexing at 1 diff --git a/src/Distribution/Server/Util/ServeTarball.hs b/src/Distribution/Server/Util/ServeTarball.hs index b4391f6d2..5d975f490 100644 --- a/src/Distribution/Server/Util/ServeTarball.hs +++ b/src/Distribution/Server/Util/ServeTarball.hs @@ -52,8 +52,9 @@ serveTarball :: (MonadIO m, MonadPlus m) -> TarIndex -- index for tarball -> [CacheControl] -> ETag -- the etag + -> Maybe (BS.ByteString -> BS.ByteString) -- optional transform to files -> ServerPartT m Response -serveTarball descr indices tarRoot tarball tarIndex cacheCtls etag = do +serveTarball descr indices tarRoot tarball tarIndex cacheCtls etag transform = do rq <- askRq action GET $ remainingPath $ \paths -> do @@ -74,7 +75,7 @@ serveTarball descr indices tarRoot tarball tarIndex cacheCtls etag = do Just (TarIndex.TarFileEntry off) -> do cacheControl cacheCtls etag - tfe <- liftIO $ serveTarEntry tarball off path + tfe <- liftIO $ serveTarEntry_ transform tarball off path ok (toResponse tfe) _ -> mzero @@ -116,22 +117,30 @@ renderDirIndex descr topdir topentries = loadTarEntry :: FilePath -> TarIndex.TarEntryOffset -> IO (Either String (Tar.FileSize, BS.ByteString)) -loadTarEntry tarfile off = do +loadTarEntry = loadTarEntry_ Nothing + +loadTarEntry_ :: Maybe (BS.ByteString -> BS.ByteString) -> FilePath -> TarIndex.TarEntryOffset -> IO (Either String (Tar.FileSize, BS.ByteString)) +loadTarEntry_ transform tarfile off = do htar <- openFile tarfile ReadMode hSeek htar AbsoluteSeek (fromIntegral $ off * 512) header <- BS.hGet htar 512 case Tar.read header of (Tar.Next Tar.Entry{Tar.entryContent = Tar.NormalFile _ size} _) -> do body <- BS.hGet htar (fromIntegral size) - return $ Right (size, body) + case transform of + Just f -> let x = f body in return $ Right (BS.length x, x) + Nothing -> return $ Right (size, body) _ -> fail "failed to read entry from tar file" serveTarEntry :: FilePath -> TarIndex.TarEntryOffset -> FilePath -> IO Response -serveTarEntry tarfile off fname = do - Right (size, body) <- loadTarEntry tarfile off - return . setHeader "Content-Length" (show size) - . setHeader "Content-Type" mimeType - $ resultBS 200 body +serveTarEntry = serveTarEntry_ Nothing + +serveTarEntry_ :: Maybe (BS.ByteString -> BS.ByteString) -> FilePath -> TarIndex.TarEntryOffset -> FilePath -> IO Response +serveTarEntry_ transform tarfile off fname = do + Right (size, body) <- loadTarEntry_ transform tarfile off + return . ((setHeader "Content-Length" (show size)) . + (setHeader "Content-Type" mimeType)) $ + resultBS 200 body where mimeType = mime fname constructTarIndexFromFile :: FilePath -> IO TarIndex diff --git a/src/Distribution/Server/Util/Validators.hs b/src/Distribution/Server/Util/Validators.hs index 2c247bbbf..1a16869a9 100644 --- a/src/Distribution/Server/Util/Validators.hs +++ b/src/Distribution/Server/Util/Validators.hs @@ -4,41 +4,26 @@ module Distribution.Server.Util.Validators , guardValidLookingEmail ) where -import Data.Char (isSpace, isPrint) -import qualified Data.Text as T +import Data.Text (Text) +import Distribution.Pretty (prettyShow) import Distribution.Server.Framework -import Distribution.Server.Users.Types (isValidUserNameChar) +import Distribution.Server.Util.Validators.Internal (validName, validUserName, validEmail) -guardValidLookingName :: T.Text -> ServerPartE () -guardValidLookingName str = either errBadUserName return $ do - guard (T.length str <= 70) ?! "Sorry, we didn't expect names to be longer than 70 characters." - guard (T.all isPrint str) ?! "Unexpected character in name, please use only printable Unicode characters." +guardValidLookingName :: Text -> ServerPartE () +guardValidLookingName = + either (errBadUserName . prettyShow) return . validName -guardValidLookingUserName :: T.Text -> ServerPartE () -guardValidLookingUserName str = either errBadRealName return $ do - guard (T.length str <= 50) ?! "Sorry, we didn't expect login names to be longer than 50 characters." - guard (T.all isValidUserNameChar str) ?! "Sorry, login names have to be ASCII characters only or _, no spaces or other symbols." +guardValidLookingUserName :: Text -> ServerPartE () +guardValidLookingUserName = + either (errBadRealName . prettyShow) return . validUserName -- Make sure this roughly corresponds to the frontend validation in user-details-form.html.st -guardValidLookingEmail :: T.Text -> ServerPartE () -guardValidLookingEmail str = either errBadEmail return $ do - guard (T.length str <= 100) ?! "Sorry, we didn't expect email addresses to be longer than 100 characters." - guard (T.all isPrint str) ?! "Unexpected character in email address, please use only printable Unicode characters." - guard hasAtSomewhere ?! "Oops, that doesn't look like an email address." - guard (T.all (not.isSpace) str) ?! "Oops, no spaces in email addresses please." - guard (T.all (not.isAngle) str) ?! "Please use just the email address, not \"name\" style." - where - isAngle c = c == '<' || c == '>' - hasAtSomewhere = case T.span (/= '@') str of - (before, rest) - | Just (_, after) <- T.uncons rest -> - T.length before >= 1 - && T.length after > 0 - && not ('@' `T.elem` after) - _ -> False +guardValidLookingEmail :: Text -> ServerPartE () +guardValidLookingEmail = + either (errBadEmail . prettyShow) return . validEmail errBadUserName, errBadRealName, errBadEmail :: String -> ServerPartE a -errBadUserName err = errBadRequest "Problem with login name" [MText err] -errBadRealName err = errBadRequest "Problem with name" [MText err] +errBadUserName err = errBadRequest "Problem with login name" [MText err] +errBadRealName err = errBadRequest "Problem with name" [MText err] errBadEmail err = errBadRequest "Problem with email address" [MText err] diff --git a/src/Distribution/Server/Util/Validators/Internal.hs b/src/Distribution/Server/Util/Validators/Internal.hs new file mode 100644 index 000000000..67e7893bf --- /dev/null +++ b/src/Distribution/Server/Util/Validators/Internal.hs @@ -0,0 +1,153 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Purely functional version of "Distribution.Server.Util.Validators" +-- for testing the validators. + +module Distribution.Server.Util.Validators.Internal where + +import Control.Monad (unless) +import Control.Monad.Except (MonadError(..)) + +import Data.Char (isSpace, isPrint) +import Data.Text (Text) +import qualified Data.Text as T + +import Distribution.Pretty (Pretty(..)) +import Distribution.Server.Users.Types (isValidUserNameChar) + +-- Set up doctest to deal with text literals. + +-- $setup +-- >>> :set -XOverloadedStrings + +-- | Basic sanity checking on names. +-- +-- >>> validName "Innocent User" +-- Right () +-- +-- >>> validName "Mr. X is the greatest super duper dude of all!" +-- Right () +-- +-- >>> validName "I am also a developer, maintainer, blogger, for Haskell, Hackage, Cabal, Stackage" +-- Left NameTooLong +-- +-- >>> validName "My name has beeps \BEL, newlines \n, and \t tabs" +-- Left NameNotPrintable +-- +validName :: Text -> Either InvalidName () +validName str = do + unless (T.length str <= 70) $ throwError NameTooLong + unless (T.all isPrint str) $ throwError NameNotPrintable + +-- | Errors produced by 'validName' check. + +data InvalidName + = NameTooLong -- ^ More than 70 characters long. + | NameNotPrintable -- ^ Contains unprintable characters. + deriving (Eq, Show) + +instance Pretty InvalidName where + pretty = \case + NameTooLong -> "Sorry, we didn't expect names to be longer than 70 characters." + NameNotPrintable -> "Unexpected character in name, please use only printable Unicode characters." + +-- | Basic sanity checking on user names. +-- +-- >>> validUserName "innocent_user_42" +-- Right () +-- +-- >>> validUserName "mr_X_stretches_the_Limit_of_50_characters_01234567" +-- Right () +-- +-- >>> validUserName "01234" +-- Right () +-- +-- >>> validUserName "dashes-not-allowed" +-- Left UserNameInvalidChar +-- +-- >>> validUserName "questions_not_allowed?" +-- Left UserNameInvalidChar +-- +-- >>> validUserName "my_Ego_busts_the_Limit_of_50_characters_01234567890" +-- Left UserNameTooLong +-- +validUserName :: T.Text -> Either InvalidUserName () +validUserName str = do + unless (T.length str <= 50) $ throwError UserNameTooLong + unless (T.all isValidUserNameChar str) $ throwError UserNameInvalidChar + +-- | Errors produced by 'validUserName' check. + +data InvalidUserName + = UserNameTooLong -- ^ More than 50 characters long. + | UserNameInvalidChar -- ^ Contains character not matching 'isValidUserNameChar'. + deriving (Eq, Show) + +instance Pretty InvalidUserName where + pretty = \case + UserNameTooLong -> "Sorry, we didn't expect login names to be longer than 50 characters." + UserNameInvalidChar -> "Sorry, login names have to be ASCII characters only or _, no spaces or other symbols." + +-- | Basic sanity checking in email. +-- +-- >>> validEmail "Emmanuel.Lauterbach@phantasy-promi.darknet.de" +-- Right () +-- +-- >>> validEmail "gerd.lauchkopf+spam@posteo.de" +-- Right () +-- +-- >>> validEmail "Emmanuel.Lauterbachs.Cousin@mailrelay.tor.amazon-aws.bill-me.cold-fusion.bogus-domain.phantasy-promi.darknet.de" +-- Left EmailTooLong +-- +-- >>> validEmail "\BELlingcat@a\nonymous.\to" +-- Left EmailNotPrintable +-- +-- >>> validEmail "ich-im-aether" +-- Left EmailBadFormat +-- +-- >>> validEmail "ich@guuugle@kom" +-- Left EmailBadFormat +-- +-- >>> validEmail "Windows User @ Company . com" +-- Left EmailHasSpace +-- +-- >>> validEmail "Name" +-- Left EmailHasAngle +-- +validEmail :: T.Text -> Either InvalidEmail () +validEmail str = do + unless (T.length str <= 100) $ throwError EmailTooLong + unless (T.all isPrint str) $ throwError EmailNotPrintable + unless hasAtSomewhere $ throwError EmailBadFormat + unless (T.all (not.isSpace) str) $ throwError EmailHasSpace + unless (T.all (not.isAngle) str) $ throwError EmailHasAngle + where + isAngle c = c == '<' || c == '>' + hasAtSomewhere = case T.break (== '@') str of + (before, rest) + | Just (_, after) <- T.uncons rest -> + not $ or + [ T.null before + , T.null after + , '@' `T.elem` after + ] + | otherwise -> False + +-- | Errors produced by 'validEmail' check. + +data InvalidEmail + = EmailTooLong -- ^ More than 100 characters long. + | EmailNotPrintable -- ^ Contains unprintable characters. + | EmailBadFormat -- ^ Doesn't have exactly one @ sign. + | EmailHasSpace -- ^ Contains spaces. + | EmailHasAngle -- ^ Contains angle brackets. + deriving (Eq, Show) + +instance Pretty InvalidEmail where + pretty = \case + EmailTooLong -> "Sorry, we didn't expect email addresses to be longer than 100 characters." + EmailNotPrintable -> "Unexpected character in email address, please use only printable Unicode characters." + EmailBadFormat -> "Oops, that doesn't look like an email address." + EmailHasSpace -> "Oops, no spaces in email addresses please." + EmailHasAngle -> "Please use just the email address, not \"name\" style." diff --git a/tests/RevDepCommon.hs b/tests/RevDepCommon.hs new file mode 100644 index 000000000..a7f79ff9c --- /dev/null +++ b/tests/RevDepCommon.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +module RevDepCommon where + +import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Char8 as Char8 +import qualified Data.Vector as Vector +import Data.Time (UTCTime(..), fromGregorian) +import GHC.Generics (Generic) + +import Distribution.Server.Users.Types (UserId(..)) +import Distribution.Package (PackageIdentifier(..), PackageName, mkPackageName, unPackageName) +import Distribution.Server.Packages.Types (PkgInfo(..)) +import Distribution.Server.Packages.Types (CabalFileText(..)) +import Distribution.Version (mkVersion, versionNumbers) + +data Package b = + Package + { pName :: b + , pVersion :: Int + , pDeps :: [ b ] + } + deriving (Ord, Show, Eq) + +packToPkgInfo :: Show b => Package b -> PkgInfo +packToPkgInfo Package {pName, pVersion, pDeps} = + mkPackage (mkPackageName $ show pName) [pVersion] (depsToBS pDeps) + +mkPackage :: PackageName -> [Int] -> [BSL.ByteString] -> PkgInfo +mkPackage name intVersion depends = + let + version = mkVersion intVersion + -- e.g. "2.3" for [2,3] + dotVersion :: BSL.ByteString + dotVersion = BSL.fromStrict . Char8.intercalate "." . map (Char8.pack . show) $ versionNumbers version + cabalFilePrefix :: BSL.ByteString + cabalFilePrefix = "\ +\name: " <> BSL.fromStrict (Char8.pack $ unPackageName name) <> "\n\ +\version: " <> dotVersion <> "\n" + cabalFile :: CabalFileText + cabalFile = CabalFileText $ cabalFilePrefix <> if depends /= [] then "library\n build-depends: " <> BSL.intercalate "," depends else "" + in + PkgInfo + (PackageIdentifier name version) + (Vector.fromList [(cabalFile, (UTCTime (fromGregorian 2020 1 1) 0, UserId 1))]) + mempty + +depsToBS :: Show b => [ b ] -> [BSL.ByteString] +depsToBS = + map (BSL.fromStrict . Char8.pack . show) + + +newtype TestPackage = TestPackage Word + deriving stock Generic + deriving newtype (Bounded, Enum, Eq, Ord) + +instance Show TestPackage where + show (TestPackage word) = "package" <> show word diff --git a/tests/ReverseDependenciesTest.hs b/tests/ReverseDependenciesTest.hs new file mode 100644 index 000000000..3cf3b42dc --- /dev/null +++ b/tests/ReverseDependenciesTest.hs @@ -0,0 +1,216 @@ +{-# LANGUAGE OverloadedStrings, NamedFieldPuns, TypeApplications, ScopedTypeVariables #-} +module Main where + +import qualified Data.Array as Arr +import qualified Data.Bimap as Bimap +import Data.Foldable (for_) +import Data.List (partition, foldl') +import qualified Data.Map as Map +import qualified Data.Set as Set + +import Distribution.Package (mkPackageName, packageId, packageName) +import Distribution.Server.Features.PreferredVersions.State (PreferredVersions(..), VersionStatus(NormalVersion), PreferredInfo(..)) +import Distribution.Server.Features.ReverseDependencies (ReverseFeature(..), ReverseCount(..), reverseFeature) +import Distribution.Server.Features.ReverseDependencies.State (ReverseIndex(..), addPackage, constructReverseIndex, emptyReverseIndex, getDependenciesFlat, getDependencies, getDependenciesFlatRaw, getDependenciesRaw) +import Distribution.Server.Framework.Hook (newHook) +import Distribution.Server.Framework.MemState (newMemStateWHNF) +import Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Server.Packages.Types (PkgInfo(..)) +import Distribution.Version (mkVersion, version0) + +import Test.Tasty (TestTree, defaultMain, testGroup) +import Test.Tasty.HUnit + +import qualified Hedgehog.Range as Range +import qualified Hedgehog.Gen as Gen +import Hedgehog ((===), Group(Group), MonadGen, Property, PropertyT, checkSequential, forAll, property) + +import RevDepCommon (Package(..), TestPackage(..), mkPackage, packToPkgInfo) + +mtlBeelineLens :: [PkgInfo] +mtlBeelineLens = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + -- Note that this example is a bit unrealistic + -- since these two do not depend on base... + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "lens" [0] ["mtl"] + ] + +mkRevFeat :: [PkgInfo] -> IO ReverseFeature +mkRevFeat pkgs = do + let + idx = PackageIndex.fromList pkgs + preferredVersions = + PreferredVersions + { preferredMap = mempty + , deprecatedMap = mempty + , migratedEphemeralPrefs = False + } + updateReverse <- newHook + let constructed = constructReverseIndex idx + memState <- newMemStateWHNF constructed + pure $ + reverseFeature + (pure idx) + (pure preferredVersions) + memState + updateReverse + +allTests :: TestTree +allTests = testGroup "ReverseDependenciesTest" + [ testCase "with set [beeline->mtl] and querying for mtl, we get beeline" $ do + let pkgs = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "beeline" [0] ["mtl"] + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [("beeline", (version0, Just NormalVersion))] + assertEqual "reverse dependencies must be [beeline]" ref res + , testCase "revPackageName selects only the version with an actual dependency, even if it is not the newest" $ do + let pkgs = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "mtl-tf" [9000] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "beeline" [1] ["mtl-tf"] + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [("beeline", (mkVersion [0], Nothing))] + assertEqual "reverse dependencies must be [beeline v0]" ref res + , testCase "revPackageId does select old version when queried with old reverse dependency" $ do + let mtl = mkPackage "mtl" [2,3] ["base"] + pkgs = + [ mkPackage "base" [4,15] [] + , mtl + , mkPackage "mtl-tf" [9000] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "beeline" [1] ["mtl-tf"] + ] + ReverseFeature{revPackageId} <- mkRevFeat pkgs + res <- revPackageId (packageId mtl) + -- Nothing because it is not the 'best version' + let ref = Map.fromList [("beeline", (mkVersion [0], Nothing))] + assertEqual "reverse dependencies must be [beeline v0]" ref res + , testCase "revPackageName can find multiple packages" $ do + let pkgs = + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "mario" [0] ["mtl"] + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [ ("beeline", (mkVersion [0], Just NormalVersion)) + , ("mario", (mkVersion [0], Just NormalVersion)) + ] + assertEqual "reverse dependencies must be [beeline v0, mario v0]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revPackageFlat 'base' finds [beeline, lens, mtl]" $ do + ReverseFeature{revPackageFlat} <- mkRevFeat mtlBeelineLens + res <- revPackageFlat "base" + let ref = [ ("beeline", 0), ("lens", 0), ("mtl", 2) ] + assertEqual "reverse dependencies must be [beeline v0, mario v0]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revPackageStats 'base' returns 1,3" $ do + ReverseFeature{revPackageStats} <- mkRevFeat mtlBeelineLens + res <- revPackageStats "base" + let ref = ReverseCount { directCount = 1, totalCount = 3 } + assertEqual "must be directCount=1, totalCount=3" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], queryReverseDeps 'base' returns [mtl],[beeline,lens]" $ do + ReverseFeature{queryReverseDeps} <- mkRevFeat mtlBeelineLens + res <- queryReverseDeps "base" + let ref = (["mtl"], ["beeline", "lens"]) + assertEqual "must be direct=[mtl], indirect=[beeline,lens]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revCountForAllPackages returns [(base,1,3),(mtl,2,2),(beeline,0,0),(lens,0,0)]" $ do + ReverseFeature{revCountForAllPackages} <- mkRevFeat mtlBeelineLens + res <- revCountForAllPackages + let ref = [("beeline",ReverseCount 0 0),("lens",ReverseCount 0 0),("base",ReverseCount 1 3),("mtl",ReverseCount 2 2)] + assertEqual "must match reference" ref res + , testCase "revDisplayInfo" $ do + ReverseFeature{revDisplayInfo} <- mkRevFeat mtlBeelineLens + res <- revDisplayInfo + assertEqual "beeline preferred is old" (PreferredInfo [] [] Nothing, [mkVersion [0]]) (res "beeline") + , testCase "hedgehogTests" $ do + res <- hedgehogTests + assertEqual "hedgehog test pass" True res + ] + +genPacks :: PropertyT IO [Package TestPackage] +genPacks = do + numPacks <- forAll $ Gen.int (Range.linear 1 10) + allowMultipleVersions <- forAll Gen.bool -- remember that this shrinks to False + packs <- forAll $ packsUntil allowMultipleVersions numPacks mempty + pure packs + +prop_constructRevDeps :: Property +prop_constructRevDeps = property $ do + packs <- genPacks + let idx = PackageIndex.fromList $ map packToPkgInfo packs + let ReverseIndex foldedRevDeps foldedMap foldedDeps = foldl' (packageFolder idx) emptyReverseIndex packs + let (ReverseIndex constructedRevDeps constructedMap constructedDeps) = constructReverseIndex idx + for_ (PackageIndex.allPackageNames idx) $ \name -> do + foundFolded :: Int <- Bimap.lookup name foldedMap + foundConstructed :: Int <- Bimap.lookup name constructedMap + -- they are not nessarily equal, since they may have been added in a different order! + -- so this doesn't necessarily hold: + -- foundFolded === foundConstructed + + -- but they should have the same deps + foldedPackNames <- mapM (`Bimap.lookupR` foldedMap) (foldedRevDeps Arr.! foundFolded) + constructedPackNames <- mapM (`Bimap.lookupR` constructedMap) (constructedRevDeps Arr.! foundConstructed) + Set.fromList foldedPackNames === Set.fromList constructedPackNames + + foldedDeps === constructedDeps + +prop_statsEqualsDeps :: Property +prop_statsEqualsDeps = property $ do + packs <- genPacks + let packages = map packToPkgInfo packs + let revs = constructReverseIndex $ PackageIndex.fromList packages + pkginfo <- forAll $ Gen.element packages + let name = packageName pkginfo + let directSet = getDependenciesRaw name revs + totalSet = getDependenciesFlatRaw name revs + directNames = getDependencies name revs + totalNames = getDependenciesFlat name revs + length directSet === length directNames + length totalSet === length totalNames + +packageFolder :: Show b => PackageIndex PkgInfo -> ReverseIndex -> Package b -> ReverseIndex +packageFolder index revindex (Package name _version deps) = addPackage index (mkPackageName $ show name) (map (mkPackageName . show) deps) revindex + + +genPackage :: forall m b. (MonadGen m, Enum b, Bounded b, Ord b) => b -> [Package b] -> m (Package b) +genPackage newName available = do + pVersion <- Gen.int (Range.linear 0 10) + depPacks :: [Package b] <- Gen.subsequence available + pure $ Package {pName = newName, pVersion, pDeps = map pName depPacks } + +packsUntil :: forall m b. (Ord b, Bounded b, MonadGen m, Enum b) => Bool -> Int -> [Package b] -> m [Package b] +packsUntil allowMultipleVersions limit generated | length generated < limit = do + makeNewPack <- Gen.bool -- if not new pack, just make a new version of an existing + toInsert <- + if makeNewPack || generated == mempty || not allowMultipleVersions + then + genPackage (toEnum $ length generated) generated + else do + Package { pName = prevName } <- Gen.element generated + let (prevNamePacks, nonPrevName) = partition ((== prevName) . pName) generated + depPacks :: [Package b] <- Gen.subsequence nonPrevName + let newVersion = 1 + maximum (map pVersion prevNamePacks) + pure $ Package {pName = prevName, pVersion = newVersion, pDeps = map pName depPacks} + let added = generated ++ [toInsert] + packsUntil allowMultipleVersions limit added +packsUntil _ _ generated = pure generated + +hedgehogTests :: IO Bool +hedgehogTests = + checkSequential $ Group "hedgehogTests" + [ ("prop_constructRevDeps", prop_constructRevDeps) + , ("prop_statsEqualsDeps", prop_statsEqualsDeps) + ] + +main :: IO () +main = defaultMain allTests