├── .gitattributes ├── .gitignore ├── .toodles.yaml ├── .travis.yml ├── Dockerfile.dev ├── Dockerfile.release ├── LICENSE ├── README.md ├── Setup.hs ├── app └── Main.hs ├── package.yaml ├── requirements.txt ├── src ├── Config.hs ├── License.hs ├── Parse.hs ├── Server.hs ├── ToodlesApi.hs └── Types.hs ├── stack.yaml ├── test └── Spec.hs ├── toodles-license-public-key.pem ├── toodles.cabal ├── verify.py └── web ├── css ├── bulma.min.css ├── font-awesome.min.css └── toodles.css ├── fonts ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── html └── index.html ├── img └── favicon.png └── js ├── app.js ├── jquery-3.3.1.min.js └── vue.js /.gitattributes: -------------------------------------------------------------------------------- 1 | web/js/vue.js linguist-vendored 2 | web/js/jquery-3.1.1.min.js linguist-vendored 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | pile.cabal 3 | *~ -------------------------------------------------------------------------------- /.toodles.yaml: -------------------------------------------------------------------------------- 1 | # Putting a .toodles.yaml file in the root of your repo will give toodles 2 | # any project specific configuration you'd like to set. 3 | 4 | # `ignore`: Specify a list of regular expressions. 5 | # Toodles will ignore a file that matches any of the entires 6 | ignore: 7 | - test.js 8 | - stack-work 9 | - node_modules 10 | - Spec.hs 11 | - venv 12 | - vue.js 13 | - min.js 14 | # `flags` specify other keywords you might want to scan other than TODO 15 | # Hardcoded ones include TODO, FIXME, and XXX 16 | flags: 17 | - MAYBE 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This is the simple Travis configuration, which is intended for use 2 | # on applications which do not require cross-platform and 3 | # multiple-GHC-version support. For more information and other 4 | # options, see: 5 | # 6 | # https://docs.haskellstack.org/en/stable/travis_ci/ 7 | # 8 | # Copy these contents into the root directory of your Github project in a file 9 | # named .travis.yml 10 | 11 | # Use new container infrastructure to enable caching 12 | sudo: false 13 | 14 | # Do not choose a language; we provide our own build tools. 15 | language: generic 16 | 17 | # Caching so the next build will be fast too. 18 | cache: 19 | directories: 20 | - $HOME/.stack 21 | timeout: 300 22 | 23 | # Ensure necessary system libraries are present 24 | addons: 25 | apt: 26 | packages: 27 | - libgmp-dev 28 | 29 | before_install: 30 | # Download and unpack the stack executable 31 | - mkdir -p ~/.local/bin 32 | - export PATH=$HOME/.local/bin:$PATH 33 | - travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 34 | 35 | install: 36 | # Build dependencies 37 | - stack --no-terminal --install-ghc install -j 1 aeson 38 | - stack --no-terminal --install-ghc test --only-dependencies 39 | 40 | script: 41 | # Build the package, its tests, and its docs and run the tests 42 | - stack --no-terminal test --skip-ghc-check 43 | 44 | notifications: 45 | email: 46 | recipients: 47 | - avipress@gmail.com 48 | on_success: change # default: change 49 | on_failure: always # default: always 50 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM haskell:8.4.3 as build-env 2 | 3 | WORKDIR /toodles-app 4 | 5 | RUN stack update 6 | 7 | COPY package.yaml /toodles-app/ 8 | COPY stack.yaml /toodles-app/ 9 | COPY app/ /toodles-app/app 10 | COPY src/ /toodles-app/src 11 | COPY test/ /toodles-app/test 12 | COPY web/ /toodles-app/web 13 | COPY README.md /toodles-app/ 14 | COPY toodles-license-public-key.pem /toodles-app/ 15 | COPY verify.py /toodles-app/ 16 | 17 | RUN stack install --only-dependencies 18 | 19 | RUN stack install 20 | 21 | FROM debian:stretch 22 | 23 | WORKDIR /toodles-app 24 | COPY --from=build-env /toodles-app . 25 | COPY --from=build-env /root/.local/bin/toodles /usr/local/bin/ 26 | 27 | VOLUME /repo 28 | 29 | EXPOSE 9001 30 | 31 | # Due to issues described in https://github.com/aviaviavi/toodles/issues/54, we 32 | # have to install stack to make the binary from the previous step work in our 33 | # container 34 | RUN apt-get update 35 | RUN apt-get install -y wget build-essential 36 | RUN wget -qO- https://get.haskellstack.org/ | sh 37 | 38 | CMD ["toodles","-d","/repo/"] 39 | -------------------------------------------------------------------------------- /Dockerfile.release: -------------------------------------------------------------------------------- 1 | FROM debian:stretch 2 | 3 | RUN apt-get update && apt-get install -y curl wget netbase 4 | 5 | RUN curl -L https://scarf.sh/install | bash 6 | 7 | ENV PATH $PATH:~/.scarf/bin 8 | 9 | RUN ~/.scarf/bin/scarf install toodles 10 | 11 | CMD ["/root/.scarf/bin/toodles","-d","/repo/"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Avi Press 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toodles 2 | 3 | [![Hackage](https://img.shields.io/hackage/v/toodles.svg)](https://hackage.haskell.org/package/toodles) 4 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/toodles-todos/community) 5 | 6 | Toodles scrapes your entire repository for TODO entries and organizes them so 7 | you can manage your project directly from the code. View, filter, sort, and edit 8 | your TODO's with an easy to use web application. When you make changes via 9 | toodles, the edits will be applied directly the TODO entries in your code. 10 | When you're done, commit and push your changes to share them with your team! 11 | 12 | ![Toodles Screenshot](https://i.imgur.com/DEwzMYn.png) 13 | 14 | ### TODO details 15 | 16 | Specify details about your TODO's so that you can filter and sort them with 17 | ease! Specify details within parenthesis and separate with the `|` delimeter. 18 | 19 | ```python 20 | # TODO(assignee|p=1|keys=vals|#tags) 21 | ``` 22 | 23 | #### Priority 24 | 25 | The key `p=` will be interpreted as a priority number 26 | 27 | #### KeyVals 28 | 29 | Use arbitrary key value pairs `=|=|...` and design any 30 | organization scheme you wish! A good use for this is to enter dates of deadlines 31 | for TODO's that you can sort on in Toodles 32 | 33 | #### Tags 34 | 35 | A detail starting with `#`, eg `#bug|#techdebt|#database|...` will be interpreted as 36 | a tag, which can be used to label and group your TODO's. 37 | 38 | #### Assign 39 | 40 | Assign your TODO's to someone. Any plain word that will be interpreted as an assignee. 41 | 42 | ```python 43 | # TODO(bob) - something we need to do later 44 | ``` 45 | 46 | ### Per Project Configuration 47 | 48 | You can configure toodles by putting a `.toodles.yaml` file in the root of your 49 | project. See this repo's `.toodles.yaml` for the full configuration spec. 50 | 51 | Currently via config you can: 52 | 53 | - Set files to ignore via a list of regular expressions 54 | - Specify your own flags to scan for other than the built-ins (TODO, FIXME, XXX) 55 | 56 | #### Ignoring Files 57 | 58 | Ignore as many files as you can! Large autogenerated files will slow Toodles 59 | down quite a bit. Check the output of the server to see any files/folders that 60 | may be causing slowness for your repo and add them to the `ignore` section your 61 | `.toodles.yaml` If the performance of Toodles is not good enough for your use 62 | case, please open an issue. 63 | 64 | ### Scanned Languages 65 | 66 | These languages will be scanned for any TODO's: 67 | 68 | - C/C++ 69 | - C# 70 | - CSS/SASS 71 | - Elixir 72 | - Erlang 73 | - Go 74 | - Haskell 75 | - HTML 76 | - Java 77 | - Javascript 78 | - Kotlin 79 | - Lua 80 | - Objective-C 81 | - PHP 82 | - Plaintext files (`*.txt`) 83 | - Protobuf 84 | - Python 85 | - React Javascript (JSX) 86 | - Ruby 87 | - Rust 88 | - Scala 89 | - Shell / Bash 90 | - Swift 91 | - Typescript 92 | - Vue (scripts only) 93 | - Yaml 94 | 95 | Submit a PR if you'd like a language to be added. There will eventually be 96 | support for this to be user configurable 97 | 98 | ### Installing with Docker 99 | 100 | You can run a pre-built toodles for your current directory via docker: 101 | 102 | ```bash 103 | # execute toodles for the directory you are currently in: 104 | $ docker run -it -v $(pwd):/repo -p 9001:9001 docker.avi.press/aviaviavi/toodles 105 | ``` 106 | 107 | Just mount your project into the container's `/repo` and direct a port of your 108 | choice to the container's `9001`. 109 | 110 | ### Building Toodles 111 | 112 | Toodles builds with [stack](https://docs.haskellstack.org). Just a `stack 113 | build` should do it. If you'd like to build toodles without cloning the source, 114 | you can have stack build toodles with `stack install --resolver=lts-12.14 toodles`! 115 | 116 | #### Building with Docker 117 | 118 | For convenience this repository also provides a `Dockerfile` to automatically 119 | build toodles. 120 | 121 | ```bash 122 | # to build container run: 123 | $ cd /path/to/toodles/repo 124 | $ docker build -t toodles Dockerfile.dev . 125 | # afterwards you can run the following command to execute toodles for the 126 | # directory you are currently in: 127 | $ docker run -it -v $(pwd):/repo -p 9001:9001 toodles 128 | 129 | ``` 130 | 131 | ### Running 132 | 133 | Invoking `toodles` with no arguments will treat the current directory as the 134 | project root and will start a server on port 9001. You can set these with the 135 | `-d` and `-p` flags, respectively. 136 | 137 | 138 | ```bash 139 | # $ toodles -d -p 140 | # for more info run: 141 | # $ toodles --help 142 | $ toodles -d /path/to/your/project -p 9001 143 | # or simply 144 | $ toodles 145 | ``` 146 | 147 | ### Contributing 148 | 149 | Contributions in any form are welcome! A few bits of info: 150 | 151 | - Don't be shy, ask questions! Contributing to Toodles should be welcoming for 152 | people at any level of programming familiarity. Whether it's a new feature, 153 | bug fix, or docs, any contribution is very appreciated. 154 | - Open an issue or jump into the chat room on [gitter](https://gitter.im/toodles-todos/community) 155 | - Before you start coding, please comment or mark a particular issue as "in 156 | progress", or even open your pull request as a work in progress (WIP). This is 157 | to help avoid having multiple people work on the same thing. 158 | - If github issues don't cut it, feel free to reach out on gitter or twitter 159 | [@avi_press](https://twitter.com/avi_press) 160 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Main where 4 | 5 | import Config 6 | import License 7 | import Paths_toodles 8 | import Server 9 | import Types 10 | 11 | import Control.Monad (when) 12 | import Data.IORef (newIORef) 13 | import Data.Maybe (fromMaybe) 14 | import qualified Data.Text as T (unpack) 15 | import Network.Wai.Handler.Warp (run) 16 | import System.Directory 17 | import System.Environment 18 | import System.FilePath.Posix 19 | import Text.Printf (printf) 20 | 21 | main :: IO () 22 | main = do 23 | dataDirLocal <- (return . takeDirectory) =<< getExecutablePath 24 | dataDirBuilt <- getDataDir 25 | useBinaryLocalDataDir <- doesDirectoryExist $ dataDirLocal <> "/web" 26 | useBuiltDataDir <- doesDirectoryExist $ dataDirBuilt <> "/web" 27 | when 28 | ((not useBinaryLocalDataDir) && (not useBuiltDataDir)) 29 | (fail $ 30 | "Couldn't initialize toodles, no valid data directory found. Please file a bug on Github. Directories tried: \n" ++ 31 | dataDirLocal ++ "\n" ++ dataDirBuilt) 32 | let dataDir = 33 | if useBinaryLocalDataDir 34 | then dataDirLocal 35 | else dataDirBuilt 36 | licenseRead <- 37 | readLicense 38 | (dataDir ++ "/toodles-license-public-key.pem") 39 | "/etc/toodles/license.json" 40 | let license = (either (BadLicense) (id) licenseRead) 41 | userArgs <- toodlesArgs >>= setAbsolutePath 42 | case userArgs of 43 | (ToodlesArgs _ _ _ _ True _) -> do 44 | sResults <- runFullSearch userArgs 45 | mapM_ (putStrLn . prettyFormat) $ todos sResults 46 | _ -> do 47 | let webPort = fromMaybe 9001 $ port userArgs 48 | ref <- newIORef Nothing 49 | putStrLn $ "serving on " ++ show webPort 50 | tierRef <- newIORef license 51 | run webPort $ app $ ToodlesState ref (dataDir ++ "/web") tierRef 52 | 53 | prettyFormat :: TodoEntry -> String 54 | prettyFormat (TodoEntryHead _ l a p n entryPriority f _ _ _ _ _ _) = 55 | printf 56 | "Assignee: %s\n%s%s:%d\n%s - %s" 57 | (fromMaybe "None" a) 58 | (maybe "" (\x -> "Priority: " ++ show x ++ "\n") entryPriority) 59 | p 60 | n 61 | (show f) 62 | (unlines $ map T.unpack l) 63 | prettyFormat a = error "Invalid type for prettyFormat: " ++ show a 64 | -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: toodles 2 | version: 1.2.3 3 | github: "aviaviavi/toodles" 4 | license: MIT 5 | author: "Avi Press" 6 | maintainer: "mail@avi.press" 7 | copyright: "2019 Avi Press" 8 | category: Project Management 9 | 10 | extra-source-files: 11 | - README.md 12 | 13 | data-files: 14 | - web/js/app.js 15 | - web/js/* 16 | - web/html/* 17 | - web/css/* 18 | - web/fonts/* 19 | - web/img/* 20 | - verify.py 21 | - toodles-license-public-key.pem 22 | 23 | synopsis: Manage the TODO entries in your code 24 | description: 25 | > 26 | Toodles scrapes your entire repository for TODO entries and organizes them so 27 | you can manage your project directly from the code. View, filter, sort, and edit 28 | your TODO\'s with an easy to use web application. When you make changes via 29 | toodles, the edits will be applied directly the TODO entries in your code. 30 | When you\'re done, commit and push your changes to share them with your team! 31 | 32 | ghc-options: 33 | - -Wall 34 | - -Wcompat 35 | 36 | dependencies: 37 | - base >= 4.4.0 && < 5 38 | 39 | # TODO (avi|p=3|#dependencies) - dependencies need to be relaxed and 40 | # fixed to include other ghc versions 41 | library: 42 | source-dirs: src 43 | exposed-modules: 44 | - Parse 45 | - Types 46 | - Config 47 | - ToodlesApi 48 | - Server 49 | - License 50 | dependencies: 51 | - MissingH >=1.4.0.1 52 | - RSA >=2.3.0 53 | - aeson ==1.3.1.1 54 | - base64-bytestring ==1.0.0.1 55 | - blaze-html ==0.9.1.1 56 | - bytestring >=0.10.8.2 57 | - cmdargs ==0.10.20 58 | - directory ==1.3.1.5 59 | - extra ==1.6.13 60 | - megaparsec ==6.5.0 61 | - process >=1.6.3.0 62 | - regex-posix ==0.95.2 63 | - servant ==0.14.1 64 | - servant-blaze ==0.8 65 | - servant-server ==0.14.1 66 | - strict ==0.3.2 67 | - text ==1.2.3.1 68 | - time >=1.8.0.2 69 | - wai ==3.2.1.2 70 | - warp ==3.2.25 71 | - yaml ==0.8.32 72 | 73 | executables: 74 | toodles: 75 | main: Main.hs 76 | source-dirs: 77 | - app 78 | - src 79 | ghc-options: 80 | - -threaded 81 | - -rtsopts 82 | - -O3 83 | - -Wall 84 | - -with-rtsopts=-N 85 | dependencies: 86 | - MissingH >=1.4.0.1 87 | - RSA >=2.3.0 88 | - aeson ==1.3.1.1 89 | - base64-bytestring ==1.0.0.1 90 | - blaze-html ==0.9.1.1 91 | - bytestring >=0.10.8.2 92 | - cmdargs ==0.10.20 93 | - directory ==1.3.1.5 94 | - extra ==1.6.13 95 | - filepath ==1.4.2 96 | - megaparsec ==6.5.0 97 | - process >=1.6.3.0 98 | - regex-posix ==0.95.2 99 | - servant ==0.14.1 100 | - servant-blaze ==0.8 101 | - servant-server ==0.14.1 102 | - strict ==0.3.2 103 | - text ==1.2.3.1 104 | - time >=1.8.0.2 105 | - wai ==3.2.1.2 106 | - warp ==3.2.25 107 | - yaml ==0.8.32 108 | 109 | tests: 110 | toodles-test: 111 | main: Spec.hs 112 | source-dirs: 113 | - test 114 | - src 115 | ghc-options: 116 | - -threaded 117 | - -rtsopts 118 | - -with-rtsopts=-N 119 | - -w 120 | dependencies: 121 | - toodles 122 | - MissingH >=1.4.0.1 123 | - aeson ==1.3.1.1 124 | - base64-bytestring ==1.0.0.1 125 | - blaze-html ==0.9.1.1 126 | - bytestring >=0.10.8.2 127 | - cmdargs ==0.10.20 128 | - directory ==1.3.1.5 129 | - extra ==1.6.13 130 | - hspec >= 2.4.4 131 | - hspec-expectations >=0.8.2 132 | - megaparsec ==6.5.0 133 | - process >=1.6.3.0 134 | - regex-posix ==0.95.2 135 | - servant ==0.14.1 136 | - servant-blaze ==0.8 137 | - servant-server ==0.14.1 138 | - strict ==0.3.2 139 | - text ==1.2.3.1 140 | - time >=1.8.0.2 141 | - wai ==3.2.1.2 142 | - warp ==3.2.25 143 | - yaml ==0.8.32 144 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycrypto 2 | -------------------------------------------------------------------------------- /src/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveDataTypeable #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE TypeOperators #-} 6 | 7 | module Config 8 | ( toodlesArgs 9 | , ToodlesArgs(..) 10 | , SearchFilter(..) 11 | , AssigneeFilterRegex(..) 12 | ) 13 | where 14 | 15 | import Paths_toodles 16 | import Types 17 | 18 | import Data.Text (Text) 19 | import Data.Version (showVersion) 20 | import System.Console.CmdArgs 21 | 22 | toodlesArgs :: IO ToodlesArgs 23 | toodlesArgs = cmdArgs argParser 24 | 25 | data ToodlesArgs = ToodlesArgs 26 | { directory :: FilePath 27 | , assignee_search :: Maybe SearchFilter 28 | , limit_results :: Int 29 | , port :: Maybe Int 30 | , no_server :: Bool 31 | , userFlag :: [UserFlag] 32 | } deriving (Show, Data, Typeable, Eq) 33 | 34 | newtype SearchFilter = 35 | AssigneeFilter AssigneeFilterRegex 36 | deriving (Show, Data, Eq) 37 | 38 | newtype AssigneeFilterRegex = AssigneeFilterRegex Text 39 | deriving (Show, Data, Eq) 40 | 41 | argParser :: ToodlesArgs 42 | argParser = ToodlesArgs 43 | { directory = def &= typFile &= help "Root directory of your project" 44 | , assignee_search = def &= help "Filter todo's by assignee" 45 | , limit_results = def &= help "Limit number of search results" 46 | , port = def &= help "Run server on port" 47 | , no_server = def &= help "Output matching todos to the command line and exit" 48 | , userFlag = def &= help "Additional flagword (e.g.: MAYBE)" 49 | } &= summary ("toodles " ++ showVersion version) 50 | &= program "toodles" 51 | &= verbosity 52 | &= help "Manage TODO's directly from your codebase" 53 | -------------------------------------------------------------------------------- /src/License.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | 7 | 8 | module License 9 | ( 10 | UserTier(..), 11 | ToodlesLicense(..), 12 | readLicense 13 | ) where 14 | 15 | import Paths_toodles 16 | 17 | import Data.Aeson 18 | import qualified Data.ByteString.Base64.Lazy as B64 19 | import qualified Data.ByteString.Lazy.Char8 as LB 20 | import Data.Text (Text) 21 | import qualified Data.Text as T 22 | import Data.Time.Clock.POSIX 23 | import GHC.Generics 24 | import System.Directory 25 | import System.Exit 26 | import System.Process 27 | 28 | data UserTier 29 | = BadLicense String 30 | | NoLiscense 31 | | Individual 32 | | Commercial 33 | deriving (Show, Eq, Ord, Generic, ToJSON, FromJSON) 34 | 35 | data License = License { 36 | payload :: ToodlesLicense, 37 | encoded :: Text, 38 | payloadSignature :: Text 39 | } deriving (Generic, FromJSON, ToJSON, Show) 40 | 41 | data ToodlesLicense = ToodlesLicense 42 | { validStart :: Integer 43 | , validEnd :: Integer 44 | , email :: Text 45 | , reference :: Text 46 | , product :: Text 47 | } deriving (Generic, FromJSON, ToJSON, Show) 48 | 49 | readLicense :: FilePath -> FilePath -> IO (Either String UserTier) 50 | readLicense publicKeyPath licensePath = do 51 | licenseExists <- doesFileExist licensePath 52 | if not licenseExists then 53 | return $ Right NoLiscense 54 | else do 55 | parsedContents <- eitherDecodeFileStrict licensePath 56 | either (return . Left) (isLicenseValid publicKeyPath) parsedContents 57 | 58 | isLicenseValid :: FilePath -> License -> IO (Either String UserTier) 59 | isLicenseValid publicKeyPath (License _ encodedPayload sig) = do 60 | dataDir <- getDataDir 61 | now <- ((* 1000) . round) `fmap` getPOSIXTime 62 | -- dependencies for license verification. for now, python 63 | -- TODO(#techdebt) - license verification should be done in haskell, this removed 64 | let args = 65 | [ dataDir ++ "/verify.py" 66 | , publicKeyPath 67 | , T.unpack sig 68 | , T.unpack encodedPayload 69 | ] 70 | decodedPayload = 71 | decode (B64.decodeLenient . LB.pack $ T.unpack encodedPayload) 72 | (exitcode, stdout, stderr) <- readProcessWithExitCode "python" args "" 73 | putStrLn stderr 74 | return $ 75 | let validated = ("True" == T.strip (T.pack stdout)) 76 | in if (exitcode == ExitSuccess) && 77 | validated && (maybe 0 validEnd decodedPayload >= now) 78 | then Right Commercial 79 | else Left "Invalid license file" 80 | -------------------------------------------------------------------------------- /src/Parse.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | 4 | module Parse where 5 | 6 | import Types 7 | 8 | 9 | import Data.Functor 10 | import Data.List (find) 11 | import Data.Maybe (fromJust, fromMaybe, isJust, 12 | isNothing) 13 | import Data.Text (Text) 14 | import qualified Data.Text as T 15 | import Data.Void (Void) 16 | import Text.Megaparsec 17 | import Text.Megaparsec.Char 18 | import qualified Text.Megaparsec.Char.Lexer as L 19 | import Text.Printf 20 | import Text.Read 21 | 22 | type Parser = Parsec Void Text 23 | 24 | symbol :: Text -> Parser Text 25 | symbol = L.symbol space 26 | 27 | parseComment :: TodoParserState -> Text -> Parser TodoEntry 28 | parseComment state fileExtension = 29 | if state == ParseStateMultiLineComment 30 | then do 31 | let closingParser = symbol $ getMultiClosingForFileType fileExtension 32 | lineWithClosing <- optional . try $ manyTill anyChar closingParser 33 | lineWithOutClosing <- optional $ many anyChar 34 | return $ TodoBodyLine (T.pack (fromMaybe (fromJust lineWithOutClosing) lineWithOutClosing)) False (isJust lineWithClosing) 35 | else do 36 | single <- optional . try $ manyTill anyChar (symbol $ getCommentForFileType fileExtension) 37 | multi <- 38 | if isJust single 39 | then return Nothing 40 | else 41 | optional . try $ manyTill anyChar (symbol $ getMultiOpeningForFileType fileExtension) 42 | if isJust single || isJust multi 43 | then do 44 | b <- many anyChar 45 | return $ TodoBodyLine (T.pack b) (isJust multi) (getMultiClosingForFileType fileExtension `T.isInfixOf` T.pack b) 46 | else 47 | fail "No open comment marker found" 48 | 49 | getCommentForFileType :: Text -> Text 50 | getCommentForFileType fileExtension = 51 | getCommentForFileTypeWithDefault singleCommentStart fileExtension 52 | 53 | getMultiClosingForFileType :: Text -> Text 54 | getMultiClosingForFileType fileExtension = 55 | getCommentForFileTypeWithDefault multiLineClose fileExtension 56 | 57 | getMultiOpeningForFileType :: Text -> Text 58 | getMultiOpeningForFileType fileExtension = 59 | getCommentForFileTypeWithDefault multiLineOpen fileExtension 60 | 61 | getCommentForFileTypeWithDefault :: (FileTypeDetails -> Maybe Text) -> Text -> Text 62 | getCommentForFileTypeWithDefault getter fileExtension = 63 | fromMaybe unkownMarker (getter =<< find (\a -> extension a == adjustedExtension) fileTypeToComment) 64 | where 65 | adjustedExtension = 66 | if T.isPrefixOf "." fileExtension 67 | then fileExtension 68 | else "." <> fileExtension 69 | 70 | integer :: Parser Integer 71 | integer = lexeme $ L.signed space L.decimal 72 | 73 | where 74 | lexeme :: Parser a -> Parser a 75 | lexeme = L.lexeme space 76 | 77 | parseAssignee :: Parser String 78 | parseAssignee = many (noneOf [')', '|', '=']) 79 | 80 | -- TODO (avi|p=3|#cleanup) - fix and type this better 81 | parseDetails :: Text -> (Maybe Text, Maybe Text, [(Text, Text)], [Text]) 82 | parseDetails toParse = 83 | let dataTokens = T.splitOn "|" toParse 84 | assigneeTo = 85 | find 86 | (\t -> 87 | not (T.null t) && 88 | not (T.isInfixOf "=" t) && not (T.isPrefixOf "#" t)) 89 | dataTokens 90 | allDetails = 91 | map (\[a, b] -> (a, b)) $ filter (\t -> length t == 2) $ 92 | map (T.splitOn "=") dataTokens 93 | priorityVal = snd <$> find (\t -> T.strip (fst t) == "p") allDetails 94 | filteredDetails = filter (\t -> T.strip (fst t) /= "p") allDetails 95 | entryTags = filter (T.isPrefixOf "#") dataTokens 96 | in (assigneeTo, priorityVal, filteredDetails, entryTags) 97 | 98 | -- | parse "hard-coded" flags, and user-defined flags if any 99 | parseFlag :: [UserFlag] -> Parser Flag 100 | parseFlag = foldr (\a b -> b <|> foo a) (try parseFlagHardcoded) 101 | where 102 | foo :: UserFlag -> Parser Flag 103 | foo (UserFlag x) = try (symbol x $> (UF $ UserFlag x)) 104 | 105 | -- | parse flags TODO, FIXME, XXX 106 | parseFlagHardcoded :: Parser Flag 107 | parseFlagHardcoded = 108 | try (symbol "TODO" $> TODO ) 109 | <|> try (symbol "FIXME" $> FIXME) 110 | <|> (symbol "XXX" $> XXX ) 111 | 112 | newtype MultineCommentEnclosing = MultineCommentEnclosing (Text, Text) 113 | 114 | data FileTypeDetails = FileTypeDetails 115 | { extension :: Text 116 | , singleCommentStart :: Maybe Text 117 | , multilineEnclosing :: Maybe MultineCommentEnclosing 118 | } 119 | 120 | multiLineClose :: FileTypeDetails -> Maybe Text 121 | multiLineClose (FileTypeDetails _ _ (Just (MultineCommentEnclosing (_, b)))) = Just b 122 | multiLineClose _ = Nothing 123 | 124 | multiLineOpen :: FileTypeDetails -> Maybe Text 125 | multiLineOpen (FileTypeDetails _ _ (Just (MultineCommentEnclosing (a, _)))) = Just a 126 | multiLineOpen _ = Nothing 127 | 128 | enclosing :: (Text, Text) -> Maybe MultineCommentEnclosing 129 | enclosing = Just . MultineCommentEnclosing 130 | 131 | slashStar :: Maybe MultineCommentEnclosing 132 | slashStar = enclosing ("/*", "*/") 133 | 134 | fileTypeToComment :: [FileTypeDetails] 135 | fileTypeToComment = 136 | [ FileTypeDetails ".c" (Just "//") slashStar 137 | , FileTypeDetails ".cc" (Just "//") slashStar 138 | , FileTypeDetails ".clj" (Just ";;") Nothing 139 | , FileTypeDetails ".cpp" (Just "//") slashStar 140 | , FileTypeDetails ".cxx" (Just "//") slashStar 141 | , FileTypeDetails ".c++" (Just "//") slashStar 142 | , FileTypeDetails ".cs" (Just "//") slashStar 143 | , FileTypeDetails ".css" Nothing slashStar 144 | , FileTypeDetails ".ex" (Just "#") Nothing 145 | , FileTypeDetails ".erl" (Just "%") Nothing 146 | , FileTypeDetails ".go" (Just "//") slashStar 147 | , FileTypeDetails ".h" (Just "//") slashStar 148 | , FileTypeDetails ".hh" (Just "//") slashStar 149 | , FileTypeDetails ".hpp" (Just "//") slashStar 150 | , FileTypeDetails ".hs" (Just "--") (enclosing ("{-", "-}")) 151 | , FileTypeDetails ".html" Nothing (enclosing ("")) 152 | , FileTypeDetails ".hxx" (Just "//") slashStar 153 | , FileTypeDetails ".h++" (Just "//") slashStar 154 | , FileTypeDetails ".java" (Just "//") slashStar 155 | , FileTypeDetails ".js" (Just "//") slashStar 156 | , FileTypeDetails ".jsx" (Just "//") slashStar 157 | , FileTypeDetails ".kt" (Just "//") slashStar 158 | , FileTypeDetails ".kts" (Just "//") slashStar 159 | , FileTypeDetails ".lua" (Just "--") (enclosing ("--[[", "]]")) 160 | , FileTypeDetails ".m" (Just "//") slashStar 161 | , FileTypeDetails ".php" (Just "//") slashStar 162 | , FileTypeDetails ".proto" (Just "//") slashStar 163 | , FileTypeDetails ".py" (Just "#") (enclosing ("\"\"\"", "\"\"\"")) 164 | , FileTypeDetails ".rb" (Just "#") (enclosing ("=begin", "=end")) 165 | , FileTypeDetails ".rs" (Just "//") slashStar 166 | , FileTypeDetails ".scss" Nothing slashStar 167 | , FileTypeDetails ".scala" (Just "//") slashStar 168 | , FileTypeDetails ".sh" (Just "#") slashStar 169 | , FileTypeDetails ".swift" (Just "///") slashStar 170 | , FileTypeDetails ".ts" (Just "//") slashStar 171 | , FileTypeDetails ".tsx" (Just "//") slashStar 172 | , FileTypeDetails ".txt" (Just "") Nothing 173 | , FileTypeDetails ".vue" (Just "//") slashStar 174 | , FileTypeDetails ".yaml" (Just "#") Nothing 175 | ] 176 | 177 | singleLineCommentForExtension :: Text -> Maybe Text 178 | singleLineCommentForExtension fileExtension = 179 | find (\f -> extension f == fileExtension) fileTypeToComment >>= singleCommentStart 180 | 181 | multiLineOpenCommentForExtension :: Text -> Maybe Text 182 | multiLineOpenCommentForExtension fileExtension = 183 | find (\f -> extension f == fileExtension) fileTypeToComment >>= 184 | multilineEnclosing >>= 185 | (\(MultineCommentEnclosing a) -> return $ fst a) 186 | 187 | -- Higher order function returning our folder. 188 | fileParseFoldFn :: 189 | -- partial fn params 190 | [UserFlag] 191 | -> FilePath 192 | -- returns a fold function of: 193 | -> (TodoParserState, [Maybe TodoEntry]) 194 | -> (Integer, Text) 195 | -> (TodoParserState, [Maybe TodoEntry]) 196 | fileParseFoldFn userFlags file (currentLineState, pastList) (lineNum, line) = 197 | let parsedLine = parseMaybe (parseTodo currentLineState userFlags file lineNum) line 198 | newState = nextState currentLineState parsedLine in 199 | (newState, pastList ++ [parsedLine]) 200 | 201 | nextState :: TodoParserState -> Maybe TodoEntry -> TodoParserState 202 | nextState _ Nothing = ParseStateUnknown 203 | nextState _ (Just (TodoBodyLine _ _ True)) = ParseStateUnknown 204 | nextState _ (Just (TodoBodyLine _ True False)) = ParseStateMultiLineComment 205 | nextState s (Just (TodoBodyLine _ False False)) = s 206 | nextState _ (Just (TodoEntryHead _ _ _ _ _ _ _ _ _ _ _ True False)) = ParseStateMultiLineComment 207 | nextState _ (Just (TodoEntryHead _ _ _ _ _ _ _ _ _ _ MultiLine _ False)) = ParseStateMultiLineComment 208 | nextState _ (Just TodoEntryHead {}) = ParseStateUnknown 209 | nextState a b = error ("No next state for " ++ show a ++ " " ++ show b) 210 | 211 | runTodoParser :: [UserFlag] -> SourceFile -> [TodoEntry] 212 | runTodoParser us (SourceFile path ls) = 213 | let parsedTodoLines = 214 | foldl 215 | (fileParseFoldFn 216 | us 217 | path) 218 | (ParseStateUnknown, []) 219 | (zip [1 ..] ls) 220 | groupedTodos = foldl foldTodoHelper ([], False) (snd parsedTodoLines) 221 | in fst groupedTodos 222 | 223 | where 224 | -- fold fn to concatenate todos that a multiple, single line comments 225 | foldTodoHelper :: ([TodoEntry], Bool) -> Maybe TodoEntry -> ([TodoEntry], Bool) 226 | foldTodoHelper (todoEntries, currentlyBuildingTodoLines) maybeTodo 227 | -- We're not on a todo line, keep going 228 | | isNothing maybeTodo = (todoEntries, False) 229 | -- We see the start of a new todo 230 | | isEntryHead $ fromJust maybeTodo = (todoEntries ++ [fromJust maybeTodo], True) 231 | -- We a body line of a todo to concatenate to the current one 232 | | isBodyLine (fromJust maybeTodo) && currentlyBuildingTodoLines = 233 | (init todoEntries ++ [combineTodo (last todoEntries) (fromJust maybeTodo)], True) 234 | | otherwise = (todoEntries, False) 235 | 236 | where 237 | isEntryHead :: TodoEntry -> Bool 238 | isEntryHead TodoEntryHead {} = True 239 | isEntryHead _ = False 240 | 241 | isBodyLine :: TodoEntry -> Bool 242 | isBodyLine TodoBodyLine {} = True 243 | isBodyLine _ = False 244 | 245 | combineTodo :: TodoEntry -> TodoEntry -> TodoEntry 246 | combineTodo (TodoEntryHead i b a p n entryPriority f attrs entryTags entryLeadingText t isOpened _) (TodoBodyLine l _ isClosed) = 247 | TodoEntryHead i (b ++ [l]) a p n entryPriority f attrs entryTags entryLeadingText t isOpened isClosed 248 | combineTodo (TodoBodyLine l isOpened _) (TodoEntryHead i b a p n entryPriority f attrs entryTags entryLeadingText t _ isClosed) = 249 | TodoEntryHead i (l : b) a p n entryPriority f attrs entryTags entryLeadingText t isOpened isClosed 250 | combineTodo (TodoBodyLine l isOpened _) (TodoBodyLine r _ isClosed) = 251 | TodoBodyLineCombined (l : [r]) isOpened isClosed 252 | combineTodo (TodoBodyLineCombined l isOpened _) (TodoBodyLine r _ isClosed) = 253 | TodoBodyLineCombined (l ++ [r]) isOpened isClosed 254 | combineTodo (TodoBodyLineCombined l isOpened _) (TodoEntryHead i b a p n entryPriority f attrs entryTags entryLeadingText t _ isClosed) = 255 | TodoEntryHead i (l ++ b) a p n entryPriority f attrs entryTags entryLeadingText t isOpened isClosed 256 | combineTodo _ _ = error "Can't combine todoEntry of these types" 257 | 258 | getExtension :: FilePath -> Text 259 | getExtension path = last $ T.splitOn "." (T.pack path) 260 | 261 | stringToMaybe :: Text -> Maybe Text 262 | stringToMaybe t = 263 | if T.null t 264 | then Nothing 265 | else Just t 266 | 267 | fst4 :: (a, b, c, d) -> a 268 | fst4 (x, _, _, _) = x 269 | 270 | snd4 :: (a, b, c, d) -> b 271 | snd4 (_, x, _, _) = x 272 | 273 | thd4 :: (a, b, c, d) -> c 274 | thd4 (_, _, x, _) = x 275 | 276 | fth4 :: (a, b, c, d) -> d 277 | fth4 (_, _, _, x) = x 278 | 279 | unkownMarker :: Text 280 | unkownMarker = "UNKNOWN-DELIMETER-UNKNOWN-DELIMETER-UNKNOWN-DELIMETER" 281 | 282 | data TodoParserState 283 | = ParseStateUnknown 284 | | ParseStateSource 285 | | ParseStateSingleComment 286 | | ParseStateMultiLineComment deriving (Eq, Show) 287 | 288 | data LeadingTextKind = SingleLT | MultiLT | NonOpenedComment deriving (Eq, Show) 289 | 290 | takeShorter :: String -> String -> String 291 | takeShorter singleLeadingText multiLeadingText 292 | | length singleLeadingText == length multiLeadingText = "" 293 | | null singleLeadingText = multiLeadingText 294 | | null multiLeadingText = singleLeadingText 295 | | length singleLeadingText > length multiLeadingText = multiLeadingText 296 | | length singleLeadingText < length multiLeadingText = singleLeadingText 297 | | otherwise = "" 298 | 299 | parseTodo :: TodoParserState -> [UserFlag] -> FilePath -> LineNumber -> Parser TodoEntry 300 | parseTodo state us path lineNum = try (parseTodoEntryHead us) 301 | <|> parseComment state (getExtension path) 302 | where 303 | parseTodoEntryHead :: [UserFlag] -> Parser TodoEntry 304 | parseTodoEntryHead uf = 305 | if state == ParseStateMultiLineComment 306 | then do 307 | leadingTextMulti <- optional (try $ many spaceChar) 308 | parseEntryHead NonOpenedComment (fromMaybe "" leadingTextMulti) 309 | else do 310 | entryLeadingTextSingle <- optional (try (manyTill anyChar (lookAhead . prefixParserForFileType $ getExtension path))) 311 | entryLeadingTextMulti <- 312 | if isNothing entryLeadingTextSingle 313 | then 314 | optional (manyTill anyChar (lookAhead . multiPrefixParserForFileType $ getExtension path)) 315 | else 316 | return Nothing 317 | 318 | commentOpenSingle <- optional . try $ prefixParserForFileType $ getExtension path 319 | commentOpenMulti <- optional . try $ multiPrefixParserForFileType $ getExtension path 320 | let leadingTextKind = case (commentOpenSingle, commentOpenMulti) of 321 | (Just _, Nothing) -> SingleLT 322 | (Nothing, Just _) -> MultiLT 323 | (Nothing, Nothing) -> NonOpenedComment 324 | err -> error . printf "Error: unexpected value in leading text pattern match: %s. Please report this bug https://github.com/aviaviavi/toodles" $ show err 325 | 326 | -- select the shorter leading text, and update leadingTextKind enum accordingly 327 | matchingLeadingText = takeShorter (fromMaybe "" entryLeadingTextSingle) (fromMaybe "" entryLeadingTextMulti) 328 | 329 | parseEntryHead leadingTextKind matchingLeadingText 330 | 331 | where 332 | 333 | parseEntryHead leadingTextCharsKind leadingTextChars = do 334 | f <- parseFlag uf 335 | entryDetails <- optional $ try (inParens $ many (noneOf [')', '('])) 336 | let parsedDetails = parseDetails . T.pack <$> entryDetails 337 | entryPriority = (readMaybe . T.unpack) =<< (snd4 =<< parsedDetails) 338 | otherDetails = maybe [] thd4 parsedDetails 339 | entryTags = maybe [] fth4 parsedDetails 340 | _ <- optional $ symbol "-" 341 | _ <- optional $ symbol ":" 342 | let closingParser = symbol $ getMultiClosingForFileType (getExtension path) 343 | lineWithClosing <- optional . try $ manyTill anyChar closingParser 344 | lineWithOutClosing <- optional $ many anyChar 345 | return $ 346 | TodoEntryHead 347 | 0 348 | [T.pack (fromMaybe (fromJust lineWithOutClosing) lineWithClosing)] 349 | (stringToMaybe . T.strip $ fromMaybe "" (fst4 =<< parsedDetails)) 350 | path 351 | lineNum 352 | entryPriority 353 | f 354 | otherDetails 355 | entryTags 356 | (T.pack leadingTextChars) 357 | (if leadingTextCharsKind == SingleLT then SingleLine else MultiLine) 358 | (leadingTextCharsKind == MultiLT) 359 | (isJust lineWithClosing) 360 | 361 | inParens :: Parser a -> Parser a 362 | inParens = between (symbol "(") (symbol ")") 363 | 364 | prefixParserForFileType :: Text -> Parser Text 365 | prefixParserForFileType fileExtension = symbol . getCommentForFileType $ fileExtension 366 | 367 | multiPrefixParserForFileType :: Text -> Parser Text 368 | multiPrefixParserForFileType fileExtension = symbol . getMultiOpeningForFileType $ fileExtension 369 | -------------------------------------------------------------------------------- /src/Server.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE TypeOperators #-} 6 | 7 | module Server where 8 | 9 | import Config 10 | import License 11 | import Parse 12 | import Paths_toodles 13 | import ToodlesApi 14 | import Types 15 | 16 | import qualified Control.Exception as E 17 | import Control.Monad 18 | import Control.Monad.IO.Class 19 | import Data.Aeson (FromJSON) 20 | import Data.Aeson.Types 21 | import Data.Either 22 | import Data.IORef 23 | import Data.List (find, nub) 24 | import Data.Maybe 25 | import Data.String.Utils 26 | import Data.Text (Text) 27 | import qualified Data.Text as T 28 | import qualified Data.Yaml as Y 29 | import Servant 30 | import System.Directory 31 | import System.Directory.Extra 32 | import qualified System.IO.Strict as SIO 33 | import System.Path.NameManip 34 | import Text.Blaze.Html5 (Html) 35 | import qualified Text.Blaze.Html5 as BZ 36 | import Text.Printf 37 | import Text.Regex.Posix 38 | 39 | freeResultsLimit :: Int 40 | freeResultsLimit = 100 41 | 42 | data ToodlesConfig = ToodlesConfig 43 | { ignore :: [FilePath] 44 | , flags :: [UserFlag] 45 | } deriving (Show) 46 | 47 | instance FromJSON ToodlesConfig where 48 | parseJSON (Object o) = do 49 | parsedIgnore <- o .:? "ignore" .!= [] 50 | parsedFlags <- o .:? "flags" .!= [] 51 | return $ ToodlesConfig parsedIgnore parsedFlags 52 | parseJSON invalid = typeMismatch "Invalid config" invalid 53 | 54 | app :: ToodlesState -> Application 55 | app s = serve toodlesAPI server 56 | 57 | where 58 | server :: Server ToodlesAPI 59 | server = liftIO . getFullSearchResults s 60 | :<|> deleteTodos s 61 | :<|> editTodos s 62 | :<|> getLicense s 63 | :<|> serveDirectoryFileServer (dataPath s) 64 | :<|> showRawFile s 65 | :<|> root s 66 | 67 | root :: ToodlesState -> [Text] -> Handler Html 68 | root (ToodlesState _ dPath _) path = 69 | if null path then 70 | liftIO $ BZ.preEscapedToHtml <$> readFile (dPath ++ "/html/index.html") 71 | else throwError $ err404 { errBody = "Not found" } 72 | 73 | getLicense :: ToodlesState -> Handler GetLicenseResponse 74 | getLicense (ToodlesState _ _ tierRef) = do 75 | license <- liftIO readUserTier 76 | _ <- liftIO $ atomicModifyIORef' tierRef (const (license, license)) 77 | return $ GetLicenseResponse license 78 | 79 | readUserTier :: IO UserTier 80 | readUserTier = do 81 | dataDir <- getDataDir 82 | licenseRead <- readLicense (dataDir ++ "/toodles-license-public-key.pem") "/etc/toodles/license.json" 83 | return $ either BadLicense id licenseRead 84 | 85 | showRawFile :: ToodlesState -> Integer -> Handler Html 86 | showRawFile (ToodlesState ref _ _) eId = do 87 | storedResults <- liftIO $ readIORef ref 88 | case storedResults of 89 | (Just (TodoListResult r _)) -> do 90 | let entry = find (\t -> entryId t == eId) r 91 | liftIO $ 92 | maybe 93 | (return "Not found") 94 | (\e -> addAnchors <$> readFile (sourceFile e)) 95 | entry 96 | Nothing -> error "no files to show" 97 | 98 | where 99 | addAnchors :: String -> Html 100 | addAnchors s = 101 | let codeLines = zip [1::Int ..] $ lines s 102 | in BZ.preEscapedToHtml 103 | (unlines $ 104 | map 105 | (\(i, l) -> printf "
%s
" (show i) l) 106 | codeLines) 107 | 108 | editTodos :: ToodlesState -> EditTodoRequest -> Handler Text 109 | editTodos s@(ToodlesState ref _ _) req = do 110 | storedResults <- liftIO $ readIORef ref 111 | case storedResults of 112 | (Just (TodoListResult r _)) -> do 113 | let editedList = 114 | map 115 | (\t -> 116 | if willEditTodo req t 117 | then editTodo req t 118 | else t) 119 | r 120 | editedFilteredList = filter (willEditTodo req) editedList 121 | _ <- mapM_ recordUpdates editedFilteredList 122 | _ <- updateCache s editedFilteredList 123 | return "{}" 124 | Nothing -> error "no stored todos to edit" 125 | where 126 | willEditTodo :: EditTodoRequest -> TodoEntry -> Bool 127 | willEditTodo editRequest entry = entryId entry `elem` editIds editRequest 128 | 129 | editTodo :: EditTodoRequest -> TodoEntry -> TodoEntry 130 | editTodo editRequest entry = 131 | let newAssignee = 132 | if isJust (setAssignee editRequest) && 133 | (not . T.null . fromJust $ setAssignee editRequest) 134 | then setAssignee editRequest 135 | else assignee entry 136 | newPriority = 137 | if isJust (setPriority editRequest) 138 | then setPriority editRequest 139 | else priority entry 140 | in entry 141 | { assignee = newAssignee 142 | , tags = tags entry ++ addTags editRequest 143 | , priority = newPriority 144 | , customAttributes = 145 | nub $ customAttributes entry ++ addKeyVals editRequest 146 | } 147 | 148 | recordUpdates :: MonadIO m => TodoEntry -> m () 149 | recordUpdates t = void $ updateTodoLinesInFile renderTodo t 150 | 151 | data UpdateType = UpdateTypeEdit | UpdateTypeDelete deriving (Eq) 152 | 153 | updateCache :: MonadIO m => ToodlesState -> [TodoEntry] -> m () 154 | updateCache (ToodlesState ref _ _) entries = do 155 | storedResults <- liftIO $ readIORef ref 156 | case storedResults of 157 | (Just (TodoListResult currentCache resultLimit)) -> do 158 | let idsToUpdate = map entryId entries 159 | newCache = 160 | TodoListResult 161 | ((++ entries) 162 | (filter 163 | (\item -> entryId item `notElem` idsToUpdate) 164 | currentCache)) 165 | resultLimit 166 | _ <- 167 | liftIO $ atomicModifyIORef' ref (const (Just newCache, Just newCache)) 168 | return () 169 | Nothing -> error "no stored todos to update" 170 | 171 | renderTodo :: TodoEntry -> [Text] 172 | renderTodo t = 173 | let ext = "." <> getExtension (sourceFile t) 174 | comment = 175 | if commentType t == SingleLine 176 | then fromJust $ singleLineCommentForExtension ext 177 | else fromJust $ multiLineOpenCommentForExtension ext 178 | detail = 179 | renderFlag (flag t) <> " (" <> 180 | T.pack ( 181 | Data.String.Utils.join 182 | "|" 183 | (map T.unpack $ 184 | [fromMaybe "" $ assignee t] ++ 185 | listIfNotNull 186 | (fmap (T.pack . maybe "" ((\n -> "p=" ++ n) . show)) priority t) ++ 187 | tags t ++ map (\a -> fst a <> "=" <> snd a) (customAttributes t))) <> 188 | ") " 189 | fullNoComments = mapHead (\l -> detail <> "- " <> l) $ body t 190 | commentFn = 191 | if commentType t == SingleLine 192 | then (\l -> comment <> " " <> l) 193 | else id 194 | commented = map commentFn fullNoComments 195 | in mapLast 196 | (\line -> 197 | if entryHeadClosed t 198 | then line <> " " <> getMultiClosingForFileType ext 199 | else line) . 200 | mapHead (\l -> if entryHeadOpened t then leadingText t <> getMultiOpeningForFileType ext <> " " <> l else leadingText t <> l) . 201 | mapInit 202 | (\l -> foldl (<>) "" [" " | _ <- [1 .. (T.length $ leadingText t)]] <> l) $ 203 | commented 204 | where 205 | listIfNotNull :: Text -> [Text] 206 | listIfNotNull "" = [] 207 | listIfNotNull s = [s] 208 | 209 | renderFlag :: Flag -> Text 210 | renderFlag TODO = "TODO" 211 | renderFlag FIXME = "FIXME" 212 | renderFlag XXX = "XXX" 213 | renderFlag (UF (UserFlag x)) = x 214 | 215 | -- | Given a function to emit new lines for a given todo, write that update in 216 | -- place of the current todo lines 217 | updateTodoLinesInFile :: MonadIO m => (TodoEntry -> [Text]) -> TodoEntry -> m () 218 | updateTodoLinesInFile f todo = do 219 | let startIndex = lineNumber todo - 1 220 | newLines = map T.unpack $ f todo 221 | fileLines <- liftIO $ lines <$> SIO.readFile (sourceFile todo) 222 | let updatedLines = 223 | slice 0 (fromIntegral $ startIndex - 1) fileLines ++ newLines ++ 224 | slice 225 | (fromIntegral startIndex + length (body todo)) 226 | (length fileLines - 1) 227 | fileLines 228 | liftIO $ writeFile (sourceFile todo) $ unlines updatedLines 229 | 230 | where 231 | slice :: Int -> Int -> [a] -> [a] 232 | slice a b xs = take (b - a + 1) (drop a xs) 233 | 234 | deleteTodos :: ToodlesState -> DeleteTodoRequest -> Handler Text 235 | deleteTodos (ToodlesState ref _ _) req = do 236 | storedResults <- liftIO $ readIORef ref 237 | case storedResults of 238 | (Just refVal@(TodoListResult r _)) -> do 239 | let toDelete = filter (\t -> entryId t `elem` ids req) r 240 | liftIO $ doUntilNull removeAndAdjust toDelete 241 | let remainingResults = filter (\t -> entryId t `notElem` map entryId toDelete) r 242 | let updatedResults = foldl (flip adjustLinesAfterDeletionOf) remainingResults toDelete 243 | let remainingResultsRef = refVal { todos = updatedResults } 244 | _ <- liftIO $ atomicModifyIORef' ref (const (Just remainingResultsRef, Just remainingResultsRef)) 245 | return "{}" 246 | Nothing -> error "no stored todos" 247 | 248 | where 249 | 250 | doUntilNull :: ([a] -> IO [a]) -> [a] -> IO () 251 | doUntilNull f xs = do 252 | result <- f xs 253 | if null result 254 | then return () 255 | else doUntilNull f result 256 | 257 | -- If we delete an entry, we need to decrement the line-numbers for the 258 | -- other entries that come later in the file 259 | adjustLinesAfterDeletionOf :: TodoEntry -> [TodoEntry] -> [TodoEntry] 260 | adjustLinesAfterDeletionOf deleted = 261 | map (\remaining -> 262 | if (sourceFile remaining == sourceFile deleted) && (lineNumber remaining > lineNumber deleted) 263 | then remaining { lineNumber = lineNumber remaining - (fromIntegral . length $ body deleted)} 264 | else remaining) 265 | 266 | removeAndAdjust :: MonadIO m => [TodoEntry] -> m [TodoEntry] 267 | removeAndAdjust [] = return [] 268 | removeAndAdjust (x:xs) = do 269 | removeTodoFromCode x 270 | return $ adjustLinesAfterDeletionOf x xs 271 | 272 | where 273 | removeTodoFromCode :: MonadIO m => TodoEntry -> m () 274 | removeTodoFromCode t = 275 | let opening = [getMultiOpeningForFileType $ getExtension (sourceFile t) | entryHeadOpened t] 276 | closing = [getMultiClosingForFileType $ getExtension (sourceFile t) | entryHeadClosed t] 277 | finalList = if length opening /= length closing then opening ++ closing else [] in 278 | updateTodoLinesInFile (const finalList) t 279 | 280 | setAbsolutePath :: ToodlesArgs -> IO ToodlesArgs 281 | setAbsolutePath args = do 282 | let pathOrDefault = if T.null . T.pack $ directory args 283 | then "." 284 | else directory args 285 | absolute <- normalise_path <$> absolute_path pathOrDefault 286 | return $ args {directory = absolute} 287 | 288 | getFullSearchResults :: ToodlesState -> Bool -> IO TodoListResult 289 | getFullSearchResults (ToodlesState ref _ tierRef) recompute = do 290 | result <- readIORef ref 291 | userLicense <- readIORef tierRef 292 | if recompute || isNothing result 293 | then do 294 | putStrLn "refreshing todo's" 295 | userArgs <- toodlesArgs >>= setAbsolutePath 296 | sResults <- runFullSearch (userArgs { limit_results = 0 }) 297 | atomicModifyIORef' ref (const (Just sResults, sResults)) 298 | else 299 | return $ fromMaybe (error "tried to read from the cache when there wasn't anything there") result 300 | 301 | runFullSearch :: ToodlesArgs -> IO TodoListResult 302 | runFullSearch userArgs = do 303 | let projectRoot = directory userArgs 304 | configExists <- doesFileExist $ projectRoot ++ "/.toodles.yaml" 305 | config <- if configExists 306 | then Y.decodeFileEither (projectRoot ++ "/.toodles.yaml") 307 | else return . Right $ ToodlesConfig [] [] 308 | when (isLeft config) 309 | $ putStrLn $ "[WARNING] Invalid .toodles.yaml: " ++ show config 310 | let config' = fromRight (ToodlesConfig [] []) config 311 | allFiles <- getAllFiles config' projectRoot 312 | parsedTodos <- concat <$> mapM (parseFileAndLog userArgs config') allFiles 313 | let filteredTodos = filter (filterSearch (assignee_search userArgs)) parsedTodos 314 | resultList = limitSearch filteredTodos $ limit_results userArgs 315 | indexedResults = map (\(i, r) -> r {entryId = i}) $ zip [1 ..] resultList 316 | limit = limit_results userArgs 317 | return $ TodoListResult indexedResults (limit /= 0 && (length indexedResults >= limit)) 318 | 319 | where 320 | filterSearch :: Maybe SearchFilter -> TodoEntry -> Bool 321 | filterSearch Nothing _ = True 322 | filterSearch (Just (AssigneeFilter (AssigneeFilterRegex query))) entry = fromMaybe "" (assignee entry) == query 323 | 324 | limitSearch :: [TodoEntry] -> Int -> [TodoEntry] 325 | limitSearch todoList 0 = todoList 326 | limitSearch todoList n = take n todoList 327 | 328 | parseFileAndLog :: ToodlesArgs -> ToodlesConfig -> SourceFile -> IO [TodoEntry] 329 | parseFileAndLog userArgs config f = do 330 | -- the strictness is so we can print "done" when we're actually done 331 | !_ <- putStrLn $ fullPath f 332 | !result <- return (runTodoParser (userFlag userArgs ++ flags config) f) 333 | !_ <- putStrLn "done" 334 | return result 335 | 336 | getAllFiles :: ToodlesConfig -> FilePath -> IO [SourceFile] 337 | getAllFiles (ToodlesConfig ignoredPaths _) basePath = 338 | E.catch 339 | (do putStrLn $ printf "Running toodles for path: %s" basePath 340 | files <- listFilesInside (return . not . ignorePath) basePath 341 | let validFiles = filter isValidFile files 342 | mapM 343 | (\f -> 344 | SourceFile f . (map T.pack . lines) <$> 345 | E.catch 346 | (SIO.readFile f) 347 | (\(e :: E.IOException) -> print e >> return "")) 348 | validFiles) 349 | (\(e :: E.IOException) -> 350 | putStrLn ("Error reading " ++ basePath ++ ": " ++ show e) >> return []) 351 | 352 | where 353 | 354 | ignorePath :: FilePath -> Bool 355 | ignorePath path = 356 | let p = T.pack path 357 | in T.isInfixOf "node_modules" p || T.isSuffixOf "pb.go" p || 358 | T.isSuffixOf "_pb2.py" p || 359 | any (\r -> path =~ r :: Bool) ignoredPaths 360 | 361 | fileHasValidExtension :: FilePath -> Bool 362 | fileHasValidExtension path = any (\ext -> ext `T.isSuffixOf` T.pack path) (map extension fileTypeToComment) 363 | 364 | isValidFile :: FilePath -> Bool 365 | isValidFile path = not (ignorePath path) && fileHasValidExtension path 366 | 367 | 368 | mapHead :: (a -> a) -> [a] -> [a] 369 | mapHead f (x:xs) = f x : xs 370 | mapHead _ xs = xs 371 | 372 | mapInit :: (a -> a) -> [a] -> [a] 373 | mapInit f (x:xs) = x : map f xs 374 | mapInit _ x = x 375 | 376 | mapLast :: (a -> a) -> [a] -> [a] 377 | mapLast f xs 378 | | null xs = [] 379 | | otherwise = init xs ++ [f $ last xs] 380 | -------------------------------------------------------------------------------- /src/ToodlesApi.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TypeOperators #-} 4 | 5 | module ToodlesApi where 6 | 7 | import Types 8 | 9 | import Data.Proxy (Proxy) 10 | import Data.Text (Text) 11 | import Servant 12 | import Servant.HTML.Blaze (HTML) 13 | import Text.Blaze.Html5 (Html) 14 | 15 | type ToodlesAPI = "todos" :> QueryFlag "recompute" :> Get '[JSON] TodoListResult 16 | 17 | :<|> "todos" :> "delete" :> ReqBody '[JSON] DeleteTodoRequest :> Post '[JSON] Text 18 | 19 | :<|> "todos" :> "edit" :> ReqBody '[JSON] EditTodoRequest :> Post '[JSON] Text 20 | 21 | :<|> "license" :> Post '[JSON] GetLicenseResponse 22 | 23 | :<|> "static" :> Raw 24 | 25 | :<|> "source_file" :> Capture "id" Integer :> Get '[HTML] Html 26 | 27 | :<|> CaptureAll "anything-else" Text :> Get '[HTML] Html 28 | 29 | toodlesAPI :: Proxy ToodlesAPI 30 | toodlesAPI = Proxy 31 | -------------------------------------------------------------------------------- /src/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveDataTypeable #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | 6 | module Types where 7 | 8 | import License 9 | 10 | import Data.Aeson (FromJSON, ToJSON, Value (String), parseJSON, 11 | toJSON) 12 | import Data.Aeson.Types (typeMismatch) 13 | import Data.Data 14 | import Data.IORef (IORef) 15 | import Data.String (IsString) 16 | import Data.Text (Text) 17 | import qualified Data.Text as T (unpack) 18 | import GHC.Generics (Generic) 19 | 20 | data SourceFile = SourceFile 21 | { fullPath :: FilePath 22 | , sourceLines :: [Text] 23 | } deriving (Show) 24 | 25 | type LineNumber = Integer 26 | 27 | data ToodlesState = ToodlesState 28 | { results :: IORef (Maybe TodoListResult), 29 | dataPath :: FilePath, 30 | userTier :: IORef UserTier 31 | } 32 | 33 | data CommentType = SingleLine | MultiLine deriving (Show, Eq, Generic) 34 | instance ToJSON CommentType 35 | 36 | data TodoEntry 37 | = TodoEntryHead { entryId :: Integer 38 | , body :: [Text] 39 | , assignee :: Maybe Text 40 | , sourceFile :: FilePath 41 | , lineNumber :: LineNumber 42 | , priority :: Maybe Integer 43 | , flag :: Flag 44 | , customAttributes :: [(Text, Text)] 45 | , tags :: [Text] 46 | , leadingText :: Text 47 | , commentType :: CommentType 48 | , entryHeadOpened :: Bool 49 | , entryHeadClosed :: Bool 50 | } 51 | | TodoBodyLine 52 | Text -- the body 53 | Bool -- has opening tag 54 | Bool -- has closing tag 55 | | TodoBodyLineCombined 56 | [Text] -- the body 57 | Bool -- has opening tag 58 | Bool -- has closing tag 59 | deriving (Show, Generic) 60 | instance ToJSON TodoEntry 61 | 62 | data TodoListResult = TodoListResult 63 | { todos :: [TodoEntry] 64 | , limited :: Bool 65 | } deriving (Show, Generic) 66 | instance ToJSON TodoListResult 67 | 68 | newtype DeleteTodoRequest = DeleteTodoRequest 69 | { ids :: [Integer] 70 | } deriving (Show, Generic) 71 | instance FromJSON DeleteTodoRequest 72 | 73 | data EditTodoRequest = EditTodoRequest 74 | { editIds :: [Integer] 75 | , setAssignee :: Maybe Text 76 | , addTags :: [Text] 77 | , addKeyVals :: [(Text, Text)] 78 | , setPriority :: Maybe Integer 79 | } deriving (Show, Generic) 80 | instance FromJSON EditTodoRequest 81 | 82 | data GetLicenseResponse = GetLicenseResponse 83 | { toodlesTier:: UserTier 84 | } deriving (Show, Generic) 85 | instance ToJSON GetLicenseResponse 86 | 87 | data Flag = TODO | FIXME | XXX | UF UserFlag 88 | deriving (Generic) 89 | 90 | newtype UserFlag = UserFlag Text 91 | deriving (Show, Eq, IsString, Data, Generic) 92 | 93 | instance Show Flag where 94 | show TODO = "TODO" 95 | show FIXME = "FIXME" 96 | show XXX = "XXX" 97 | show (UF (UserFlag x)) = T.unpack x 98 | 99 | instance ToJSON Flag where 100 | toJSON TODO = Data.Aeson.String "TODO" 101 | toJSON FIXME = Data.Aeson.String "FIXME" 102 | toJSON XXX = Data.Aeson.String "XXX" 103 | toJSON (UF (UserFlag x)) = Data.Aeson.String x 104 | 105 | instance FromJSON Flag where 106 | parseJSON (Data.Aeson.String x) = 107 | case x of 108 | "TODO" -> pure TODO 109 | "FIXME" -> pure FIXME 110 | "XXX" -> pure XXX 111 | _ -> pure $ UF $ UserFlag x 112 | parseJSON invalid = typeMismatch "UserFlag" invalid 113 | 114 | instance ToJSON UserFlag where 115 | toJSON (UserFlag x) = Data.Aeson.String x 116 | 117 | instance FromJSON UserFlag where 118 | parseJSON (Data.Aeson.String x) = pure $ UserFlag x 119 | parseJSON invalid = typeMismatch "UserFlag" invalid 120 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a comtoodlesr version. 8 | # A snapshot resolver dictates the comtoodlesr version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 15 | # 16 | # The location of a snapshot can be provided as a file or url. Stack assumes 17 | # a snapshot provided as a file might change, whereas a url resource does not. 18 | # 19 | # resolver: ./custom-snapshot.yaml 20 | # resolver: https://example.com/snapshots/2018-01-01.yaml 21 | resolver: lts-12.14 22 | pvp-bounds: both 23 | 24 | # User packages to be built. 25 | # Various formats can be used as shown in the example below. 26 | # 27 | # packages: 28 | # - some-directory 29 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 30 | # - location: 31 | # git: https://github.com/commercialhaskell/stack.git 32 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 33 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 34 | # subdirs: 35 | # - auto-update 36 | # - wai 37 | packages: 38 | - . 39 | # Dependency packages to be pulled from upstream that are not in the resolver 40 | # using the same syntax as the packages field. 41 | # (e.g., acme-missiles-0.3) 42 | extra-deps: [] 43 | 44 | # Override default flag values for local packages and extra-deps 45 | # flags: {} 46 | 47 | # Extra package databases containing global packages 48 | # extra-package-dbs: [] 49 | 50 | # Control whether we use the GHC we find on the path 51 | # system-ghc: true 52 | # 53 | # Require a specific version of stack, using version ranges 54 | # require-stack-version: -any # Default 55 | # require-stack-version: ">=1.7" 56 | # 57 | # Override the architecture used by stack, especially useful on Windows 58 | # arch: i386 59 | # arch: x86_64 60 | # 61 | # Extra directories used by stack for building 62 | # extra-include-dirs: [/path/to/dir] 63 | # extra-lib-dirs: [/path/to/dir] 64 | # 65 | # Allow a newer minor version of GHC than the snapshot specifies 66 | # comtoodlesr-check: newer-minor 67 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Main where 4 | 5 | import Parse 6 | import Types 7 | 8 | import Data.Either 9 | import qualified Data.Text as T 10 | import Test.Hspec 11 | import Text.Megaparsec 12 | 13 | bodyText (TodoBodyLine a _ _) = a 14 | bodyText t = T.unlines (body t) 15 | 16 | hasClosing (TodoBodyLine _ _ a) = a 17 | hasClosing t = entryHeadClosed t 18 | 19 | hasOpening (TodoBodyLine _ t _) = t 20 | hasOpening t = entryHeadOpened t 21 | 22 | defaultBodyLine = TodoBodyLine "default bad" False False 23 | 24 | main :: IO () 25 | main = hspec $ 26 | describe "Toodles" $ do 27 | let unknownStateParser = parse (parseTodo ParseStateUnknown [] "hello.hs" 10 ) "" 28 | multiLineStateParser = parse (parseTodo ParseStateMultiLineComment [] "hello.hs" 10 ) "" 29 | singleHaskellParser = parse (parseComment ParseStateSingleComment ".hs") "" 30 | multiHaskellParser = parse (parseComment ParseStateMultiLineComment ".hs") "" 31 | 32 | it "parses single line comments" $ do 33 | let haskellSingleComment = " -- a plain comment" 34 | singleParsed = singleHaskellParser haskellSingleComment 35 | singleParsed `shouldSatisfy` isRight 36 | bodyText (fromRight defaultBodyLine singleParsed) `shouldSatisfy` (`T.isInfixOf` "a plain comment") 37 | 38 | it "parses lines of multiline comments" $ do 39 | let haskellMultiComment = "a plain comment" 40 | multiParsed = multiHaskellParser haskellMultiComment 41 | multiParsed `shouldSatisfy` isRight 42 | let result = fromRight defaultBodyLine multiParsed 43 | hasClosing result `shouldBe` False 44 | 45 | let multiParsedWithClosing = multiHaskellParser (haskellMultiComment <> " -} ") 46 | multiParsedWithClosing `shouldSatisfy` isRight 47 | let result2 = fromRight defaultBodyLine multiParsedWithClosing 48 | bodyText result2 `shouldSatisfy` ((`T.isInfixOf` "a plain comment")) 49 | hasClosing result2 `shouldBe` True 50 | 51 | it "parses multiline comment closings" $ do 52 | let haskellMultiClose = "-}" 53 | let multiCloseParsed = multiHaskellParser haskellMultiClose 54 | multiCloseParsed `shouldSatisfy` isRight 55 | let result3 = fromRight defaultBodyLine multiCloseParsed 56 | hasClosing result3 `shouldBe` True 57 | hasOpening result3 `shouldBe` False 58 | 59 | it "parses multiline comment opens" $ do 60 | let emptyMultiOpen = " {- " 61 | emptyOpen = unknownStateParser emptyMultiOpen 62 | emptyOpen `shouldSatisfy` isRight 63 | let result5 = fromRight defaultBodyLine emptyOpen 64 | hasOpening result5 `shouldBe` True 65 | hasClosing result5 `shouldBe` False 66 | 67 | it "parses empty lines as multiline comments " $ do 68 | let emptyMulti = "" 69 | emptyMultiParsed = multiLineStateParser emptyMulti 70 | emptyMultiParsed `shouldSatisfy` isRight 71 | let result6 = fromRight defaultBodyLine emptyMultiParsed 72 | hasOpening result6 `shouldBe` False 73 | hasClosing result6 `shouldBe` False 74 | 75 | it "parses single TODOs" $ do 76 | let haskellSingleTodo = " -- TODO(avi|p=1|#tag|key=val) some stuff we need to fix" 77 | singleParsed = unknownStateParser haskellSingleTodo 78 | singleParsed `shouldSatisfy` isRight 79 | bodyText (fromRight defaultBodyLine singleParsed) `shouldBe` "some stuff we need to fix\n" 80 | 81 | it "parses a multiline TODO fully enclosed on one line" $ do 82 | let haskellEnclosedTodo = " {- TODO(avi|p=1|#tag|key=val) some stuff we need to fix -} " 83 | multiParsed = unknownStateParser haskellEnclosedTodo 84 | multiParsed `shouldSatisfy` isRight 85 | let result2 = fromRight defaultBodyLine multiParsed 86 | bodyText result2 `shouldBe` "some stuff we need to fix \n" 87 | hasClosing result2 `shouldBe` True 88 | hasOpening result2 `shouldBe` True 89 | 90 | it "parses mutline TODO's on an opening line" $ do 91 | let haskellMultiOpenTodo = " {- TODO(avi|p=1|#tag|key=val) some stuff we need to fix" 92 | multiOpenParsed = unknownStateParser haskellMultiOpenTodo 93 | multiOpenParsed `shouldSatisfy` isRight 94 | let result3 = fromRight defaultBodyLine multiOpenParsed 95 | bodyText result3 `shouldBe` "some stuff we need to fix\n" 96 | hasClosing result3 `shouldBe` False 97 | hasOpening result3 `shouldBe` True 98 | 99 | it "parses mutline TODO's on a closing line" $ do 100 | let haskellMultiCloseTodo = " TODO(avi|p=1|#tag|key=val) some stuff we need to fix -}" 101 | unknownStateParser haskellMultiCloseTodo `shouldSatisfy` isLeft 102 | 103 | let mutliCloseParsed = multiLineStateParser haskellMultiCloseTodo 104 | mutliCloseParsed `shouldSatisfy` isRight 105 | let result4 = fromRight defaultBodyLine mutliCloseParsed 106 | bodyText result4 `shouldBe` "some stuff we need to fix \n" 107 | hasClosing result4 `shouldBe` True 108 | hasOpening result4 `shouldBe` False 109 | 110 | it "parses bare TODO's in the middle of an opening line" $ do 111 | let haskellBareMultiTodo = " TODO(avi|p=1|#tag|key=val) some stuff we need to fix" 112 | bareMultiParsed = multiLineStateParser haskellBareMultiTodo 113 | bareMultiParsed `shouldSatisfy` isRight 114 | let result7 = fromRight defaultBodyLine bareMultiParsed 115 | hasOpening result7 `shouldBe` False 116 | hasClosing result7 `shouldBe` False 117 | bodyText result7 `shouldBe` "some stuff we need to fix\n" 118 | 119 | -------------------------------------------------------------------------------- /toodles-license-public-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvrVba8B99vGOUvHUSNSv 3 | T6idquIYp3CdhRww2VckyrVwqCeFNePm6EDhHAsG4SVd94HQDpZdJd/Oohrqlt1I 4 | UccaPse4W3J9nMJx82H9BK91VuFWLKvPAEbeQn+uStRsSeAPeIBVo+y4i5cYPqdV 5 | eN6me2r79/cNwD21auTGTsTLJzY+RaiRA+pnmJuXvxhMjyER/GUn6hikJWrSF+6e 6 | 9txokiJR9r2hCv2JQqYHWp/moF/nd/25aTrGEgiWz+RGY15P4UUk1Ju1DmNUQA4/ 7 | RmOe1XnXw0X8aoKhuhTjZw5COsq5uHTq7tqC/CemQpldsETqj19XYO3TNi7ojmfa 8 | EQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /toodles.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.31.2. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | -- 7 | -- hash: c8ccbbd4e2a437d577d621689f01748517867d25b786d1975a314d43043295b8 8 | 9 | name: toodles 10 | version: 1.2.3 11 | synopsis: Manage the TODO entries in your code 12 | description: Toodles scrapes your entire repository for TODO entries and organizes them so you can manage your project directly from the code. View, filter, sort, and edit your TODO\'s with an easy to use web application. When you make changes via toodles, the edits will be applied directly the TODO entries in your code. When you\'re done, commit and push your changes to share them with your team! 13 | category: Project Management 14 | homepage: https://github.com/aviaviavi/toodles#readme 15 | bug-reports: https://github.com/aviaviavi/toodles/issues 16 | author: Avi Press 17 | maintainer: mail@avi.press 18 | copyright: 2019 Avi Press 19 | license: MIT 20 | license-file: LICENSE 21 | build-type: Simple 22 | extra-source-files: 23 | README.md 24 | data-files: 25 | web/js/app.js 26 | web/js/jquery-3.3.1.min.js 27 | web/js/vue.js 28 | web/html/index.html 29 | web/css/bulma.min.css 30 | web/css/font-awesome.min.css 31 | web/css/toodles.css 32 | web/fonts/fontawesome-webfont.woff 33 | web/fonts/fontawesome-webfont.woff2 34 | web/img/favicon.png 35 | verify.py 36 | toodles-license-public-key.pem 37 | 38 | source-repository head 39 | type: git 40 | location: https://github.com/aviaviavi/toodles 41 | 42 | library 43 | exposed-modules: 44 | Parse 45 | Types 46 | Config 47 | ToodlesApi 48 | Server 49 | License 50 | other-modules: 51 | Paths_toodles 52 | hs-source-dirs: 53 | src 54 | ghc-options: -Wall -Wcompat 55 | build-depends: 56 | MissingH >=1.4.0.1 57 | , RSA >=2.3.0 58 | , aeson ==1.3.1.1 59 | , base >=4.4.0 && <5 60 | , base64-bytestring ==1.0.0.1 61 | , blaze-html ==0.9.1.1 62 | , bytestring >=0.10.8.2 63 | , cmdargs ==0.10.20 64 | , directory ==1.3.1.5 65 | , extra ==1.6.13 66 | , megaparsec ==6.5.0 67 | , process >=1.6.3.0 68 | , regex-posix ==0.95.2 69 | , servant ==0.14.1 70 | , servant-blaze ==0.8 71 | , servant-server ==0.14.1 72 | , strict ==0.3.2 73 | , text ==1.2.3.1 74 | , time >=1.8.0.2 75 | , wai ==3.2.1.2 76 | , warp ==3.2.25 77 | , yaml ==0.8.32 78 | default-language: Haskell2010 79 | 80 | executable toodles 81 | main-is: Main.hs 82 | other-modules: 83 | Config 84 | License 85 | Parse 86 | Server 87 | ToodlesApi 88 | Types 89 | Paths_toodles 90 | hs-source-dirs: 91 | app 92 | src 93 | ghc-options: -Wall -Wcompat -threaded -rtsopts -O3 -Wall -with-rtsopts=-N 94 | build-depends: 95 | MissingH >=1.4.0.1 96 | , RSA >=2.3.0 97 | , aeson ==1.3.1.1 98 | , base >=4.4.0 && <5 99 | , base64-bytestring ==1.0.0.1 100 | , blaze-html ==0.9.1.1 101 | , bytestring >=0.10.8.2 102 | , cmdargs ==0.10.20 103 | , directory ==1.3.1.5 104 | , extra ==1.6.13 105 | , filepath ==1.4.2 106 | , megaparsec ==6.5.0 107 | , process >=1.6.3.0 108 | , regex-posix ==0.95.2 109 | , servant ==0.14.1 110 | , servant-blaze ==0.8 111 | , servant-server ==0.14.1 112 | , strict ==0.3.2 113 | , text ==1.2.3.1 114 | , time >=1.8.0.2 115 | , wai ==3.2.1.2 116 | , warp ==3.2.25 117 | , yaml ==0.8.32 118 | default-language: Haskell2010 119 | 120 | test-suite toodles-test 121 | type: exitcode-stdio-1.0 122 | main-is: Spec.hs 123 | other-modules: 124 | Config 125 | License 126 | Parse 127 | Server 128 | ToodlesApi 129 | Types 130 | Paths_toodles 131 | hs-source-dirs: 132 | test 133 | src 134 | ghc-options: -Wall -Wcompat -threaded -rtsopts -with-rtsopts=-N -w 135 | build-depends: 136 | MissingH >=1.4.0.1 137 | , aeson ==1.3.1.1 138 | , base >=4.4.0 && <5 139 | , base64-bytestring ==1.0.0.1 140 | , blaze-html ==0.9.1.1 141 | , bytestring >=0.10.8.2 142 | , cmdargs ==0.10.20 143 | , directory ==1.3.1.5 144 | , extra ==1.6.13 145 | , hspec >=2.4.4 146 | , hspec-expectations >=0.8.2 147 | , megaparsec ==6.5.0 148 | , process >=1.6.3.0 149 | , regex-posix ==0.95.2 150 | , servant ==0.14.1 151 | , servant-blaze ==0.8 152 | , servant-server ==0.14.1 153 | , strict ==0.3.2 154 | , text ==1.2.3.1 155 | , time >=1.8.0.2 156 | , toodles 157 | , wai ==3.2.1.2 158 | , warp ==3.2.25 159 | , yaml ==0.8.32 160 | default-language: Haskell2010 161 | -------------------------------------------------------------------------------- /verify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from Crypto.PublicKey import RSA 5 | from Crypto.Signature import PKCS1_v1_5 6 | from Crypto.Hash import SHA512 7 | from base64 import b64decode, b64encode 8 | 9 | def verify_sign(public_key_loc, signature, data): 10 | with open(public_key_loc, "r") as pub_key_file: 11 | pub_key = pub_key_file.read() 12 | rsakey = RSA.importKey(pub_key) 13 | signer = PKCS1_v1_5.new(rsakey) 14 | digest = SHA512.new() 15 | digest.update(data) 16 | if signer.verify(digest, b64decode(signature)): 17 | return True 18 | return False 19 | 20 | if __name__ == "__main__": 21 | print(verify_sign(sys.argv[1], sys.argv[2], sys.argv[3])) 22 | 23 | -------------------------------------------------------------------------------- /web/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;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} 5 | -------------------------------------------------------------------------------- /web/css/toodles.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | background-color: #346df0; 3 | width: 100%; 4 | } 5 | 6 | .navbar-item { 7 | margin-left: 1em; 8 | color: white; 9 | } 10 | 11 | .side-bar { 12 | padding-left: 2em; 13 | } 14 | 15 | .content-container { 16 | margin-top: 1em; 17 | } 18 | 19 | .main-container { 20 | padding-right: 2%; 21 | } 22 | 23 | .todo-item-location { 24 | font-size: .7em; 25 | color: grey; 26 | } 27 | 28 | .filter-todos { 29 | width: 95%; 30 | } 31 | 32 | .loading-spinner { 33 | line-height: 2em; 34 | } 35 | 36 | .loading-spinner a { 37 | border: none; 38 | } 39 | 40 | .navbar-item { 41 | cursor: pointer; 42 | } 43 | 44 | .navbar-item:active { 45 | color: grey; 46 | } 47 | 48 | .priority-number { 49 | width: 5em; 50 | } 51 | 52 | .todo-source-link { 53 | font-size: .75em; 54 | } 55 | 56 | .todo-source-link:hover { 57 | text-decoration: underline; 58 | } 59 | 60 | .todo-source-link a:link, .todo-source-link a:visited { 61 | color: grey !important; 62 | } 63 | 64 | .sortable { 65 | cursor: pointer; 66 | } 67 | 68 | .tag-block{ 69 | margin-top: .3em; 70 | } 71 | 72 | .tag-column { 73 | max-width: 200px; 74 | } 75 | 76 | .todo-item-customAttributes { 77 | max-width: 200px; 78 | } 79 | 80 | .tag-item { 81 | background-color: #4F6D7A; 82 | line-height: 2; 83 | color: white; 84 | font-size: .66em; 85 | margin-right: .2em; 86 | padding: .3em; 87 | text-align: center; 88 | } 89 | 90 | .todo-item:hover { 91 | cursor: pointer; 92 | } 93 | 94 | .todo-item-body { 95 | word-wrap: break-word; 96 | } 97 | 98 | td, th { 99 | word-wrap: break-word; 100 | } 101 | 102 | .attribute-block{ 103 | } 104 | 105 | .attribute-item { 106 | background-color: #D6F6DD; 107 | margin-right: .2em; 108 | padding: .3em; 109 | line-height: 2; 110 | font-size: .66em; 111 | } 112 | 113 | .priority-column { 114 | min-width: 105px; 115 | } 116 | 117 | .modal-content { 118 | background-color: white; 119 | min-width: 50%; 120 | min-height: 20%; 121 | } 122 | 123 | .edit-todo-field-input { 124 | width: 50%; 125 | } 126 | .edit-todo-form{ 127 | padding: 2em; 128 | } 129 | 130 | .navbar-burger { 131 | color: #363636; 132 | } 133 | 134 | .is-active .navbar-item { 135 | color: black; 136 | } 137 | 138 | .is-active .navbar-item:hover { 139 | background-color: #dbdbdb; 140 | } 141 | 142 | .toodles-nav-title-text { 143 | padding-left: 5px; 144 | } 145 | 146 | .input-error { 147 | color: red; 148 | } 149 | 150 | .bad-license-banner { 151 | background-color: #d1345b; 152 | color: white; 153 | text-align: center; 154 | } 155 | 156 | .bad-license-banner a { 157 | text-decoration: underline; 158 | color: white; 159 | } 160 | 161 | .warning-banner { 162 | background-color: #FFBE0B; 163 | } 164 | 165 | -------------------------------------------------------------------------------- /web/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviaviavi/toodles/503f35f1b713ebe4daf7b7e2f9e7563e33e3a060/web/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /web/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviaviavi/toodles/503f35f1b713ebe4daf7b7e2f9e7563e33e3a060/web/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /web/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | Toodles 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 61 | 62 | 101 |
102 | 122 |
123 |
Loading Loading 124 | TODO's. If your project is large, this can take a few moments. 125 | Try configuring toodles to ignore large, autogenerated, or 126 | third-party files. 127 | More info on configuring toodles 128 |
129 |
130 |
131 | 132 | 133 | 134 | 135 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 152 | 153 | 157 | 158 | 170 | 171 |
136 | Priority 137 | 138 | 139 | 140 | FlagBodyAssigneeTags
150 |
{{ todo.priority }}
151 |
{{ todo.flag }} 154 | 155 |
{{ todo.body }}
156 |
{{ todo.assignee }} 159 |
160 | 161 | {{entry[0]}}={{entry[1]}} 162 | 163 |
164 |
165 | 166 | {{tag}} 167 | 168 |
169 |
172 |
173 |
174 |
175 |
176 |
177 | 178 | -------------------------------------------------------------------------------- /web/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviaviavi/toodles/503f35f1b713ebe4daf7b7e2f9e7563e33e3a060/web/img/favicon.png -------------------------------------------------------------------------------- /web/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | new Vue({ 3 | el: '#top-div', 4 | data: () => { 5 | return { 6 | todos: [], 7 | todoSearch: "", 8 | loading: true, 9 | priorityFilter: "any", 10 | sortMultiplier: {'priority': 1}, 11 | customSortSelected: '', 12 | customAttributeKeys: [], 13 | setAssignee: "", 14 | addTags: "", 15 | setPriority: null, 16 | lastSortField: null, 17 | addKeyVals: "", 18 | addKeyValParseError: false, 19 | nothingFilledError: false, 20 | license: null, 21 | loadingLicense: false, 22 | limited: false, 23 | } 24 | }, 25 | created: function() { 26 | this.refresh()() 27 | }, 28 | methods: { 29 | // higher order for ease of calling from vue templates 30 | refresh: function(recompute) { 31 | return function() { 32 | this.loading = true 33 | $.ajax("/todos?recompute=" + !!recompute, { 34 | type: "GET", 35 | dataType: "json", 36 | success: function(data) { 37 | this.todos = data.todos.map(t => { 38 | return { 39 | id: t.entryId, 40 | body: t.body.join("\n"), 41 | assignee: t.assignee, 42 | sourceFile: t.sourceFile, 43 | lineNumber: t.lineNumber, 44 | priority: t.priority, 45 | flag: t.flag, 46 | customAttributes: t.customAttributes.reduce((acc, curr) => { 47 | console.log(acc, curr) 48 | acc[curr[0]] = curr[1] 49 | console.log(acc, curr) 50 | return acc 51 | }, {}), 52 | tags: t.tags, 53 | selected: false 54 | } 55 | }) 56 | this.limited = data.limited 57 | this.loading = false 58 | 59 | if (!recompute) { 60 | this.sortTodos('priority') 61 | } else { 62 | const field = this.lastSortField || 'priority' 63 | this.sortMultiplier[field] *= -1 // reset the multipler 64 | this.sortTodos(field) 65 | } 66 | 67 | this.customAttributeKeys = Array.from(new Set([].concat.apply([], this.todos.map(t => { 68 | return Object.keys(t.customAttributes) 69 | })))) 70 | 71 | this.customAttributeKeys.sort() 72 | this.hideDropdown() 73 | }.bind(this), 74 | error: function() { 75 | this.loading = false 76 | alert("Error! D: Check your connection to the server") 77 | }.bind(this) 78 | }) 79 | }.bind(this) 80 | }, 81 | 82 | sortTodos: function(sortField) { 83 | this.lastSortField = sortField 84 | if (!sortField || typeof sortField !== 'string') { 85 | sortField = this.customSortSelected 86 | } 87 | // multiplier for custom sort val that does asc / desc sort 88 | const multiplier = this.sortMultiplier[sortField] || 1 89 | if (sortField === 'priority') { 90 | // so we keep nulls at the end always when sorting 91 | const valDefault = multiplier > 0 ? Number.MAX_SAFE_INTEGER : Number.MIN_SAFE_INTEGER 92 | this.todos.sort(function(a, b) { 93 | const _a = (a.priority !== null ? a.priority : valDefault) 94 | const _b = (b.priority !== null ? b.priority : valDefault) 95 | if (_a < _b) { 96 | return multiplier * -1 97 | } else if (_a > _b) { 98 | return multiplier * 1 99 | } 100 | return 0 101 | }) 102 | } else { // custom 103 | // so we keep nulls at the end always when sorting 104 | const valDefault = multiplier > 0 ? "zzzzzzzzzz" : "" 105 | this.todos.sort(function(a, b) { 106 | const _a = (a.customAttributes[sortField] || valDefault) 107 | const _b = (b.customAttributes[sortField] || valDefault) 108 | if (_a < _b) { 109 | return multiplier * -1 110 | } else if (_a > _b) { 111 | return multiplier * 1 112 | } 113 | return 0 114 | }) 115 | } 116 | 117 | this.sortMultiplier[sortField] = multiplier * -1 118 | 119 | return null 120 | }, 121 | 122 | updateTodo: function(name) { 123 | return function() { 124 | console.log(name) 125 | } 126 | }, 127 | 128 | toggleTodo: function(todo) { 129 | todo.selected = !todo.selected 130 | }, 131 | 132 | stopPropagation: function(e) { 133 | e.stopPropagation() 134 | }, 135 | 136 | editSeletedTodos: function() { 137 | $(".modal").addClass("is-active") 138 | this.hideDropdown() 139 | }, 140 | 141 | closeModal: function() { 142 | $(".modal").removeClass("is-active") 143 | }, 144 | 145 | deleteSeletedTodos: function() { 146 | if (confirm("Are you sure you want to delete these todo's?")) { 147 | $.ajax({ 148 | url: "/todos/delete", 149 | type: "POST", 150 | dataType: "json", 151 | contentType: 'application/json', 152 | data: JSON.stringify({ 153 | ids: this.todos.filter(t => t.selected).map(t => t.id) 154 | }), 155 | success: function(data){ 156 | this.todos = this.todos.filter(function(t) { 157 | return !t.selected 158 | }.bind(this)) 159 | this.hideDropdown() 160 | }.bind(this) 161 | }) 162 | } else { 163 | console.log("no") 164 | } 165 | }, 166 | 167 | selectAll: function() { 168 | this.todos.map(function(t) { 169 | t.selected = true 170 | }) 171 | this.hideDropdown() 172 | }, 173 | 174 | deselectAll: function() { 175 | this.todos.map(function(t) { 176 | t.selected = false 177 | }) 178 | this.hideDropdown() 179 | }, 180 | 181 | toggleMenuBurger: function(ev) { 182 | $(".navbar-burger").toggleClass("is-active") 183 | $(".navbar-menu").toggleClass("is-active") 184 | }, 185 | 186 | hideDropdown: function(ev) { 187 | $(".navbar-menu").removeClass("is-active") 188 | $(".navbar-burger").removeClass("is-active") 189 | }, 190 | 191 | getLicense: function() { 192 | const self = this 193 | self.loadingLicense = true 194 | $.ajax({ 195 | url: "/license", 196 | type: "POST", 197 | dataType: "json", 198 | contentType: 'application/json', 199 | success: function(data){ 200 | self.license = data.toodlesTier.tag 201 | self.loadingLicense = false 202 | }, 203 | error: function(err){ 204 | console.error(err) 205 | } 206 | }) 207 | }, 208 | 209 | submitTodoEdits: function(){ 210 | this.keyValParseError = false 211 | this.nothingFilledError = false 212 | 213 | if (!(this.addKeyVals || this.setAssignee || this.setPriority || this.addTags)) { 214 | this.nothingFilledError = true 215 | return 216 | } 217 | const keyVals = 218 | this.addKeyVals.trim() === "" ? 219 | [] : 220 | this.addKeyVals.split(",").map(p => p.split("=").map(t => t.trim())) 221 | const keyValError = keyVals.some(p => p && p.length !== 2) 222 | if (keyValError) { 223 | this.addKeyValParseError = true 224 | return 225 | } 226 | 227 | $.ajax({ 228 | url: "/todos/edit", 229 | type: "POST", 230 | dataType: "json", 231 | contentType: 'application/json', 232 | data: JSON.stringify({ 233 | editIds: this.todos.filter(t => t.selected).map(t => t.id), 234 | setAssignee: this.setAssignee, 235 | addTags: this.addTags.split(",").map(s => s.trim()).filter(s => !!s) 236 | .map(tag => { 237 | return tag[0] === "#" ? 238 | tag : 239 | "#" + tag 240 | 241 | }), 242 | setPriority: parseInt(this.setPriority), 243 | addKeyVals: keyVals, 244 | }), 245 | success: function(data){ 246 | this.todos.filter(function(t) { 247 | return t.selected 248 | }.bind(this)) 249 | .map(function(t) { 250 | if (this.setAssignee) { 251 | t.assignee = this.setAssignee 252 | } 253 | if (this.addTags) { 254 | t.tags = t.tags.concat(this.addTags) 255 | } 256 | if (this.setPriority) { 257 | t.priority = parseInt(this.setPriority) 258 | } 259 | 260 | if (this.addKeyVals) { 261 | for (var i = 0; i < keyVals.length; i++) { 262 | t.customAttributes[keyVals[i][0]] = keyVals[i][1] 263 | } 264 | } 265 | }.bind(this)) 266 | this.closeModal() 267 | this.todos.map(t => { 268 | t.selected = false 269 | }) 270 | }.bind(this), 271 | error: console.log 272 | }) 273 | } 274 | } 275 | }) 276 | }) 277 | -------------------------------------------------------------------------------- /web/js/jquery-3.3.1.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("