packages feed

notmuch-web 0.1.1 → 0.1.2

raw patch · 37 files changed

+1990/−1380 lines, 37 filesdep +HUnitdep +markdowndep −monad-controldep −pandocdep −shakespeare-cssdep ~xss-sanitize

Dependencies added: HUnit, markdown

Dependencies removed: monad-control, pandoc, shakespeare-css, shakespeare-js, shakespeare-text, yesod-form

Dependency ranges changed: xss-sanitize

Files

README.md view
@@ -2,12 +2,12 @@  This project is an email client for [notmuch](http://notmuchmail.org) written in [Haskell](http://www.haskell.org) and [Yesod](http://www.yesodweb.com).  The code implements a web-server and uses [bootstrap](http://twitter.github.com/bootstrap/) and [jquery](http://jquery.com/)+server and uses [bootstrap](http://twbs.github.io/bootstrap/) and [jquery](http://jquery.com/) for the UI.  The current features are -* Search messages --  [search screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/search.png) and-  [pager screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/pager.png)+* Search messages - [search+  screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/search.png) and [pager+  screenshot](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/screenshots/pager.png)     * link default searches from the navigation bar     * view results in a table, with customizable buttons for retagging each thread     * view results in a pager, which shows the thread content together with a navigation bar with@@ -17,17 +17,27 @@     * a visible tree structure and the ability to collapse individual messages     * download attachments from messages     * customizable tagging buttons on each message+    * On small screens (under 700 pixels width like phones), parse text messages as markdown and+      display the original text message together with the resulting markdown HTML.  This allows the+      message to be flowed to fit the small screen, but still allows one to view the original+      message.  On larger screens, the original message is displayed by default but the markdown+      HTML is available in a tab. * Compose email     * sending with attachments     * supports reply and reply all-    * for compose, I recommend [Its All+    * for composing messages, I recommend [Its All       Text](https://addons.mozilla.org/en-us/firefox/addon/its-all-text/) to use your favorite       editor.+    * An optional address book.  Addresses can either be loaded from+      [abook](http://abook.sourceforge.net/) on the server (abook must be in the path) or from+      Google Contacts (loading from Google contacts requires a setting change in settings.yml). * Execute a raw notmuch command and view the results +[ChangeLog](https://bitbucket.org/wuzzeb/notmuch-web/src/tip/ChangeLog)+ # Quickstart -To quickly test out the client on linux, make sure you have libgmp, zlib, and libicu installed,+To quickly test out the client on linux, make sure you have libgmp and zlib installed, [download the latest binary](https://bitbucket.org/wuzzeb/notmuch-web/downloads), extract the tarball, then run @@ -45,7 +55,12 @@ One nice feature of GHC (the Haskell compiler) is the ability to statically link binaries to not require Haskell to be installed.  I have therefore built the latest release; you can find the tarballs on the [download page](https://bitbucket.org/wuzzeb/notmuch-web/downloads).  The only-prerequisites are glibc, libgmp, libicu, and zlib.+prerequisites are glibc, libgmp, and zlib.  The binaries are missing one feature: use of+[libicu](http://site.icu-project.org/) to decode text/html message parts that have a charset that is+not ISO-8859 or UTF-8 (UTF-8 and ISO-8859 are decoded internally without the help of libicu).  The+reason for this restriction is that libicu is not generally binary compatible between different+versions, so I could not link against a version of libicu that worked on many distributions.  I am+working on removing this restriction so future versions should include all features.  #### Source, latest released version 
config/routes view
@@ -8,11 +8,11 @@ /                               HomeR GET /search                         SearchPostR POST /search/#String                 SearchR GET-/retag/thread/#ThreadID/#T.Text RetagThreadR POST+/retag/thread/#ThreadID/#Text RetagThreadR POST /customretag/thread/#ThreadID   CustomRetagThreadR POST  /thread/#ThreadID                 ThreadR GET-/retag/message/#MessageID/#T.Text RetagMessageR POST+/retag/message/#MessageID/#Text RetagMessageR POST /customretag/message/#MessageID   CustomRetagMessageR POST /messagepart/#MessageID/#Int      MessagePartR GET @@ -23,3 +23,5 @@ /compose             ComposeR GET POST /reply/#MessageID    ReplyR GET /replyall/#MessageID ReplyAllR GET+/previewmsg          PreviewMessageR POST+/abook/query         AbookQueryR GET
config/settings.yml view
@@ -1,7 +1,11 @@ Default: &defaults   host: "*4" # any IPv4 host-  port: 3000+  port: 3000 # can be overridden via the PORT environment variable or --port command line argument +  # AGPLv3 requires a link to the source code on every page.  If you have not changed the+  # source, you can leave it linked to my bitbucket page.+  source-link: https://bitbucket.org/wuzzeb/notmuch-web/src+   # The folders are the notmuch searches that are linked from the navigation bar.   # The first folder is displayed on the home page.   folders:@@ -13,7 +17,7 @@         search: tag:lists and tag:unread    # The retag buttons which appear for threads and messages.  The icon name is from-  # the bootstrap glyphicons-halflings (http://twitter.github.com/bootstrap/base-css.html#icons).+  # the bootstrap glyphicons-halflings (http://twbs.github.io/bootstrap/2.3.2/base-css.html#icons)   retag:       - name: Mark Read         icon: icon-ok@@ -34,13 +38,33 @@   #message-id-domain: example.com  +  # Google Contact Support +  #+  # The address fields (To, CC, BCC) on the compose form can optionally search+  # contacts either from abook locally or can search from your google contacts.+  # To enable google contacts, you must visit+  # https://code.google.com/apis/console/ click on "API Access", click on+  # "Create an OAuth Client ID", and fill in the data.  The application type+  # must be "Web application" since the contacts will be loaded directly by+  # javascript running in the browser.  The site/hostname must match where you+  # serve notmuch-web from because of CORS protection+  # (http://en.wikipedia.org/wiki/Cross-origin_resource_sharing).  The Redirect+  # URIs can be ignored since we will access google only via javascript. The+  # Client ID can then be copied into the following setting.  Alternatively, an+  # environment variable NOTMUCH_WEB_GOOGLE_CLIENTID will also be searched+  # during startup for the client ID (useful for development).++  #google-client-id: ""++ Production:   # The approot is the full URL to root of notmuch web.   approot: "http://localhost:3000" -  # The from address for compose.  Currently only a single from address is allowed.  Format should be-  #   Name <abc@example.com>-  from-address: "x"+  # List of one or more from addresses for compose.  Format should be  Name <abc@example.com>+  from-addresses:+      - "Name <abc@example.com>"+      - "Name <abc@other.example.com>"    # You must set your hashed password.   # See http://hackage.haskell.org/packages/archive/pwstore-fast/latest/doc/html/Crypto-PasswordStore.html@@ -59,7 +83,9 @@  Development:   approot: "http://localhost:3000"-  from-address: "Test <test@example.com>"+  from-addresses:+      - "Test <test@example.com>"+      - "Addr2 <addr2@example.com>"   # For development and testing, the password is hunter2   hashed-password: "sha256|12|lMzlNz0XK9eiPIYPY96QCQ==|1ZJ/R3qLEF0oCBVNtvNKLwZLpXPM7bLEy/Nc6QBxWro="   sent-box: sent@@ -67,7 +93,8 @@  Testing:   approot: "http://localhost:3000"-  from-address: "Test <test@example.com>"+  from-addresses:+      - "Test <test@example.com>"   # For development and testing, the password is hunter2   hashed-password: "sha256|12|lMzlNz0XK9eiPIYPY96QCQ==|1ZJ/R3qLEF0oCBVNtvNKLwZLpXPM7bLEy/Nc6QBxWro="   sent-box: sent@@ -75,6 +102,7 @@  Staging:   approot: "http://localhost:3000"-  from-address: "x"+  from-address:+      -  "Test <test@example.com>"   hashed-password: "x"   <<: *defaults
main.hs view
@@ -14,16 +14,21 @@ You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>. -}+import Prelude hiding (putStrLn, getLine)+ import Application          (makeApplication)-import Prelude              (IO, elem, Bool(..), ($), putStr)+import Control.Monad        (when)+import Crypto.PasswordStore (makePassword)+import Data.Text.Encoding   (encodeUtf8, decodeUtf8)+import Data.Text.IO         (getLine, putStrLn)+import Data.Version         (Version(..))+import NotmuchCmd           (notmuchVersion) import Settings             (parseExtra) import System.Environment   (getArgs)-import System.IO            (hSetEcho, hFlush, stdin, stdout)+import System.Exit          (exitWith, ExitCode(..))+import System.IO            (hSetEcho, hFlush, stdin, stdout, hPutStrLn, stderr) import Yesod.Default.Config (fromArgs) import Yesod.Default.Main   (defaultMain)-import Data.Text.IO         (getLine, putStrLn)-import Data.Text.Encoding   (encodeUtf8, decodeUtf8)-import Crypto.PasswordStore (makePassword)  genPassword :: IO () genPassword = do@@ -38,6 +43,11 @@  main :: IO () main = do+  v <- notmuchVersion+  when (v < Version {versionBranch = [0,15], versionTags = []}) $ do+      hPutStrLn stderr "notmuch-web requires at least notmuch 0.15"+      exitWith $ ExitFailure 1+   a <- getArgs   if "--make-password" `elem` a     then genPassword
messages/en.msg view
@@ -7,6 +7,7 @@ Date: Date Num: Num Subject: Subject+Body: Body Authors: Authors Attach: Attachments Tags: Tags@@ -27,3 +28,12 @@ Retag: Retag TagsToAdd: Tags to add TagsToRemove: Tags to remove+BodyFormat: Interpret body as+Preview: Preview Html+AddrBookType: Address Book Type+NoAddressBook: No address book+Abook: Abook on server+Google: Google Contacts+GoogleAuth: Authorize Google Contacts+AddrHelpTitle: Entering Email Addresses+AddrHelp: Email addresses should either be entered directly or can be entered as "Name <email@example.com>" without the quotes.  To save the email address and start entering a new one, you can press comma.  Double clicking on an existing entry will allow you to edit it.
notmuch-web.cabal view
@@ -1,5 +1,5 @@ name:              notmuch-web-version:           0.1.1+version:           0.1.2 cabal-version:     >= 1.8 build-type:        Simple synopsis:          A web interface to the notmuch email indexer@@ -13,7 +13,8 @@ description:       An email client for the notmuch email indexer (<http://notmuchmail.org>),                    built using Yesod.  This project implements a web server and uses bootstrap                    and jquery for the UI.  The client is fully functional, with searching, viewing,-                   and composing email messages.+                   and composing email messages.  See+                   <https://bitbucket.org/wuzzeb/notmuch-web/src/tip/ChangeLog> for recent changes.  data-files: README.md             config/favicon.ico@@ -46,6 +47,10 @@     Description:   Build for use with "yesod devel"     Default:       False +Flag no-icu+    Description:   Do not link against libicu+    Default:       False+ library     hs-source-dirs:  src     exposed-modules: Application@@ -55,12 +60,14 @@                      StaticFiles                      NotmuchCmd                      FilterHtml+                     Handler.Abook+                     Handler.Compose+                     Handler.ComposeFields                      Handler.Home-                     Handler.Tags-                     Handler.View                      Handler.Pager                      Handler.Raw-                     Handler.Compose+                     Handler.Tags+                     Handler.View      if flag(dev) || flag(library-only)         cpp-options:   -DDEVELOPMENT@@ -68,6 +75,11 @@     else         ghc-options:   -Wall -O2 +    if flag(no-icu)+        cpp-options:   -DNO_ICU+    else+        cpp-options:   -DUSE_ICU+     extensions: TemplateHaskell                 QuasiQuotes                 OverloadedStrings@@ -81,60 +93,102 @@                 EmptyDataDecls                 NoMonomorphismRestriction -    build-depends: base                          >= 4          && < 5+    if flag(no-icu)+        build-depends: base                          >= 4          && < 5 -                 , aeson-                 , attoparsec-                 , attoparsec-conduit-                 , blaze-builder-                 , blaze-html-                 , blaze-markup-                 , bytestring-                 , case-insensitive-                 , conduit-                 , containers-                 , data-default-                 , directory-                 , email-validate-                 , filepath-                 , hamlet-                 -- https://github.com/bos/aeson/issues/125 and https://github.com/tibbe/hashable/issues/66-                 , hashable                      <= 1.2.0.5 || > 1.2.0.7-                 , hjsmin-                 , http-conduit-                 , http-types-                 , lifted-base-                 , mime-mail-                 , monad-control-                 , network-                 , old-locale-                 , pandoc                        >= 1.11   && < 1.12-                 , process-                 , process-conduit               >= 0.5-                 , pwstore-fast-                 , random-                 , shakespeare-css-                 , shakespeare-js-                 , shakespeare-text-                 , tagsoup-                 , template-haskell-                 , text-                 , text-icu-                 , time-                 , transformers-                 , unordered-containers-                 , vector-                 , wai-                 , wai-extra-                 , warp-                 , xss-sanitize-                 , yaml-                 , yesod                         >= 1.2    && < 1.3-                 , yesod-auth-                 , yesod-core-                 , yesod-form-                 , yesod-static+                     , aeson+                     , attoparsec+                     , attoparsec-conduit+                     , blaze-builder+                     , blaze-html+                     , blaze-markup+                     , bytestring+                     , case-insensitive+                     , conduit+                     , containers+                     , directory+                     , data-default+                     , email-validate+                     , filepath+                     , hamlet+                     -- https://github.com/bos/aeson/issues/125 and https://github.com/tibbe/hashable/issues/66+                     , hashable                      <= 1.2.0.5 || > 1.2.0.7+                     , hjsmin+                     , http-conduit+                     , http-types+                     , lifted-base+                     , markdown                      >= 0.1.5+                     , mime-mail+                     , network+                     , old-locale+                     , process+                     , process-conduit               >= 0.5+                     , pwstore-fast+                     , random+                     , tagsoup+                     , template-haskell+                     , text+                     , time+                     , transformers+                     , unordered-containers+                     , vector+                     , wai+                     , wai-extra+                     , warp+                     , xss-sanitize                  >= 0.3.4+                     , yaml+                     , yesod                         >= 1.2    && < 1.3+                     , yesod-auth+                     , yesod-static+    else+        build-depends: base                          >= 4          && < 5 +                     , aeson+                     , attoparsec+                     , attoparsec-conduit+                     , blaze-builder+                     , blaze-html+                     , blaze-markup+                     , bytestring+                     , case-insensitive+                     , conduit+                     , containers+                     , data-default+                     , directory+                     , email-validate+                     , filepath+                     , hamlet+                     -- https://github.com/bos/aeson/issues/125 and https://github.com/tibbe/hashable/issues/66+                     , hashable                      <= 1.2.0.5 || > 1.2.0.7+                     , hjsmin+                     , http-conduit+                     , http-types+                     , lifted-base+                     , markdown                      >= 0.1.5+                     , mime-mail+                     , network+                     , old-locale+                     , process+                     , process-conduit               >= 0.5+                     , pwstore-fast+                     , random+                     , tagsoup+                     , template-haskell+                     , text+                     , text-icu+                     , time+                     , transformers+                     , unordered-containers+                     , vector+                     , wai+                     , wai-extra+                     , warp+                     , xss-sanitize                  >= 0.3.4+                     , yaml+                     , yesod                         >= 1.2    && < 1.3+                     , yesod-auth+                     , yesod-static+ executable         notmuch-web     if flag(library-only)         Buildable: False@@ -157,6 +211,9 @@     build-depends: base                  , notmuch-web                  , hspec+                 , HUnit+                 , mime-mail+                 , text                  , yesod                  , yesod-test                  , yesod-core
package.sh view
@@ -1,10 +1,15 @@ #!/bin/bash +args=+if [ "$1" != "--icu" ]; then+	args="-fno-icu"+fi+ f="notmuch-web-`grep ^version notmuch-web.cabal | awk '{print $2}'`" r="releases/$f"  cabal clean-cabal configure+cabal configure $args cabal build strip dist/build/notmuch-web/notmuch-web mkdir -p $r
src/Application.hs view
@@ -22,9 +22,9 @@     , makeFoundation     ) where -import Import hiding (loadConfig)-import Settings+import Import import StaticFiles (staticSite)+import System.Environment (lookupEnv) import Yesod.Auth (getAuth) import Yesod.Default.Config import Yesod.Default.Main@@ -36,22 +36,16 @@  -- Import all relevant handler modules here. -- Don't forget to add new modules to your cabal file!+import Handler.Abook+import Handler.Compose import Handler.Home-import Handler.Tags-import Handler.View import Handler.Pager import Handler.Raw-import Handler.Compose+import Handler.Tags+import Handler.View --- This line actually creates our YesodDispatch instance. It is the second half--- of the call to mkYesodData which occurs in Foundation.hs. Please see the--- comments there for more details. mkYesodDispatch "App" resourcesApp --- This function allocates resources (such as a database connection pool),--- performs initialization and creates a WAI application. This is also the--- place to put your migrate statements to have automatic database--- migrations handled by Yesod. makeApplication :: AppConfig DefaultEnv Extra -> IO Application makeApplication conf = do     foundation <- makeFoundation conf@@ -65,11 +59,22 @@ makeFoundation conf = do     manager <- newManager def     s <- staticSite++    -- Load message ID domain if it has not been set     conf' <- case (extraMessageIDDomain $ appExtra conf) of               "" -> do u <- NS.getHostName                        return $ conf { appExtra = (appExtra conf) { extraMessageIDDomain = T.pack u} }               _  -> return conf-    return $ App conf' s manager $ extraHashedPwd $ appExtra conf'++    -- Load google client id if it has not been set+    conf'' <- case (extraGoogleClientId $ appExtra conf') of+                Nothing -> do mi <- lookupEnv "NOTMUCH_WEB_GOOGLE_CLIENTID"+                              case mi of+                                 Just i -> return $ conf' { appExtra = (appExtra conf') { extraGoogleClientId = Just $ T.pack i } }+                                 Nothing -> return conf'+                Just _ -> return conf'++    return $ App conf'' s manager $ extraHashedPwd $ appExtra conf''  -- for yesod devel getApplicationDev :: IO (Int, Application)
src/FilterHtml.hs view
@@ -16,72 +16,28 @@ -} module FilterHtml (filterHtml) where --- Based on the xss-sanitize project http://hackage.haskell.org/package/xss-sanitize--- but a lot more restrictive.- import Import-import Data.Maybe (mapMaybe)-import Data.String (IsString)-import Text.HTML.SanitizeXSS (sanitizeAttribute)+import Data.Text (Text)+import Text.HTML.SanitizeXSS import Text.HTML.TagSoup-import qualified Data.Map as Map-import qualified Data.Text.Lazy as TL  -- | Filter Html-filterHtml :: TL.Text -> TL.Text-filterHtml = renderTags . balance Map.empty . safeTags . parseTags+filterHtml :: Text -> Text+filterHtml = filterTags (balanceTags . allowedTags . safeTags)  -- | List of allowed tags.-allowedTags :: [TL.Text]-allowedTags = ["div", "p", "br", "blockquote"]+allowed :: [Text]+allowed = ["div", "p", "br", "blockquote"]  -- | Filter which tags are allowed-safeTags :: [Tag TL.Text] -> [Tag TL.Text]-safeTags [] = []-safeTags (TagOpen name attrs:ts) | name `elem` allowedTags = TagOpen name (safeAttrs attrs) : safeTags ts-safeTags (TagOpen _    _    :ts)                           = safeTags ts--safeTags (t@(TagClose name):ts) | name `elem` allowedTags = t : safeTags ts-safeTags   (TagClose _:ts)                                = safeTags ts--safeTags (t:ts) = t : safeTags ts---- | Filter the attributes.-safeAttrs :: [Attribute TL.Text] -> [Attribute TL.Text]-safeAttrs = sanitizeAttribute' . filterAttrs-    where-        filterAttrs = filter $ \(n,_) -> n `elem` okAttrs-        okAttrs = ["style", "class"]+allowedTags :: [Tag Text] -> [Tag Text]+allowedTags [] = []+allowedTags (t@(TagOpen name _):ts)+    | name `elem` allowed = t : allowedTags ts+    | otherwise           = allowedTags ts --- | Wrapper around xss-sanitize's sanitizeAttribute.  --- --- This clears bad attributes but also filters the CSS within the style attribute, removing--- things like urls and so forth.-sanitizeAttribute' :: [Attribute TL.Text] -> [Attribute TL.Text]-sanitizeAttribute' = map fromStrict . mapMaybe sanitizeAttribute . map toStrict-    where-        fromStrict (a,b) = (TL.fromStrict a, TL.fromStrict b)-        toStrict (a,b) = (TL.toStrict a, TL.toStrict b)+allowedTags (t@(TagClose name):ts)+    | name `elem` allowed = t : allowedTags ts+    | otherwise           = allowedTags ts --- Copied from xss-sanitize and updated to work with lazy text by changing the type signature.-balance :: (IsString a, Ord a) => Map.Map a Int -> [Tag a] -> [Tag a]-balance m [] =-    concatMap go $ Map.toList m-  where-    go (name, i) | name == "br" = []-                 | otherwise    = replicate i $ TagClose name-balance m (t@(TagClose name):tags) =-    case Map.lookup name m of-        Nothing -> TagOpen name [] : TagClose name : balance m tags-        Just i ->-            let m' = if i == 1-                        then Map.delete name m-                        else Map.insert name (i - 1) m-             in t : balance m' tags-balance m (TagOpen name as : tags) =-    TagOpen name as : balance m' tags-  where-    m' = case Map.lookup name m of-            Nothing -> Map.insert name 1 m-            Just i -> Map.insert name (i + 1) m-balance m (t:ts) = t : balance m ts+allowedTags (t:ts) = t : allowedTags ts
src/Foundation.hs view
@@ -17,68 +17,41 @@ module Foundation where  import Prelude++import Blaze.ByteString.Builder.Char.Utf8 (fromText)+import Control.Applicative+import Data.ByteString (ByteString)+import Data.Maybe (isJust)+import Data.Monoid (mappend)+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import Network.HTTP.Conduit (Manager)+import Network.Wai (requestHeaders)+import NotmuchCmd (ThreadID, MessageID)+import Settings (widgetFile, Extra (..), development)+import StaticFiles+import Text.Hamlet (hamletFile)+import Text.Jasmine (minifym) import Yesod import Yesod.Auth-import Yesod.Static import Yesod.Default.Config import Yesod.Default.Util (addStaticContentExternal)-import Network.HTTP.Conduit (Manager)-import Data.Monoid (mappend)-import qualified Settings-import StaticFiles-import Settings (widgetFile, Extra (..))-import Network.Wai (requestHeaders)-import Data.Maybe (isJust)-import Text.Jasmine (minifym)-import Text.Hamlet (hamletFile)-import NotmuchCmd (ThreadID, MessageID)-import Control.Applicative-import Blaze.ByteString.Builder.Char.Utf8 (fromText)-import Data.Text (Text)+import Yesod.Static import qualified Crypto.PasswordStore as PS-import qualified Data.ByteString as B-import qualified Data.Text as T-import qualified Data.Text.Encoding as T --- | The site argument for your application. This can be a good place to--- keep settings and values requiring initialization before your application--- starts running, such as database connections. Every handler will have--- access to the data present here. data App = App     { settings :: AppConfig DefaultEnv Extra     , getStatic :: Static -- ^ Settings for static file serving.     , httpManager :: Manager-    , passwordHash :: B.ByteString -- ^ hashed password from "Crypto.PasswordStore"+    , passwordHash :: ByteString -- ^ hashed password from "Crypto.PasswordStore"     } --- Set up i18n messages. See the message folder. mkMessage "App" "messages" "en" --- This is where we define all of the routes in our application. For a full--- explanation of the syntax, please see:--- http://www.yesodweb.com/book/handler------ This function does three things:------ * Creates the route datatype AppRoute. Every valid URL in your---   application can be represented as a value of this type.--- * Creates the associated type:---       type instance Route App = AppRoute--- * Creates the value resourcesApp which contains information on the---   resources declared below. This is used in Handler.hs by the call to---   mkYesodDispatch------ What this function does *not* do is create a YesodSite instance for--- App. Creating that instance requires all of the handler functions--- for our application to be in scope. However, the handler functions--- usually require access to the AppRoute datatype. Therefore, we--- split these actions into two functions and place them in separate files. mkYesodData "App" $(parseRoutesFile "config/routes")  type Form x = Html -> MForm (HandlerT App IO) (FormResult x, Widget) --- Please see the documentation for the Yesod typeclass. There are a number--- of settings which can be configured by overriding methods here. instance Yesod App where     approot = ApprootMaster $ appRoot . settings @@ -103,11 +76,12 @@         master <- getYesod         mmsg <- getMessage         let folders = extraFolders $ appExtra $ settings master+        sourceLink <- extraSourceLink <$> getExtra          pjax <- isPjax         if pjax           then do pc <- widgetToPageContent widget-                  hamletToRepHtml $ pageBody pc+                  giveUrlRenderer $ pageBody pc            else do pc <- widgetToPageContent $ do                           addStylesheet $ StaticR css_bootstrap_min_css@@ -121,12 +95,7 @@                            $(widgetFile "default-layout") -                  hamletToRepHtml $(hamletFile "templates/default-layout-wrapper.hamlet")--    -- This is done to provide an optimization for serving static files from-    -- a separate domain. Please see the staticRoot setting in Settings.hs-    urlRenderOverride y (StaticR s) =-        Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s+                  giveUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")      -- The opensearch.xml file must include {searchTerms} in the url template, but     -- the default url renderer percent encodes the braces which doesn't work.  So we@@ -143,8 +112,8 @@     -- and names them based on a hash of their content. This allows     -- expiration dates to be set far in the future without worry of     -- users receiving stale content.-    addStaticContent = addStaticContentExternal mini base64md5 Settings.staticDir (StaticR . flip StaticRoute [])-      where mini = if Settings.development then Right else minifym+    addStaticContent = addStaticContentExternal mini base64md5 "static" (StaticR . flip StaticRoute [])+      where mini = if development then Right else minifym      -- Place Javascript at bottom of the body tag so the rest of the page loads first     jsLoader _ = BottomOfBody@@ -152,10 +121,8 @@     -- What messages should be logged. The following includes all messages when     -- in development, and warnings and errors in production.     shouldLog _ _source level =-        Settings.development || level == LevelWarn || level == LevelError+        development || level == LevelWarn || level == LevelError --- This instance is required to use forms. You can modify renderMessage to--- achieve customized and internationalized form validation messages. instance RenderMessage App FormMessage where     renderMessage _ _ = defaultFormMessage @@ -168,12 +135,12 @@ isPjax = do r <- waiRequest             return $ isJust $ lookup "X-PJAX" $ requestHeaders r -loginForm :: AForm (HandlerT App IO) B.ByteString-loginForm = T.encodeUtf8 <$> areq passwordField pwd Nothing+loginForm :: AForm (HandlerT App IO) ByteString+loginForm = encodeUtf8 <$> areq passwordField pwd Nothing   where pwd = FieldSettings (SomeMessage MsgPassword) Nothing (Just "Password") Nothing []  instance YesodAuth App where-    type AuthId App = T.Text+    type AuthId App = Text     loginDest _  = HomeR     logoutDest _ = HomeR     getAuthId (Creds _ n _) = return $ Just n
+ src/Handler/Abook.hs view
@@ -0,0 +1,47 @@+{-+Copyright (C) 2013 John Lenz <lenz@math.uic.edu>++This program is free software: you can redistribute it and/or modify+it under the terms of the GNU General Public License as published by+the Free Software Foundation, either version 3 of the License, or+(at your option) any later version.++This program is distributed in the hope that it will be useful,+but WITHOUT ANY WARRANTY; without even the implied warranty of+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+GNU General Public License for more details.++You should have received a copy of the GNU General Public License+along with this program.  If not, see <http://www.gnu.org/licenses/>.+-}+module Handler.Abook (getAbookQueryR) where++import Import+import Control.Exception.Lifted (try)+import Data.Conduit.Process+import System.Exit (ExitCode(..))+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Conduit.List as C+import qualified Data.Conduit.Text as C++-- | Abook returns an entry as "<email addr><tab><name><tab>".  This function parses+-- such a line into a JSON value.+parseLine :: T.Text -> Value+parseLine x = case T.split (=='\t') x of+                [] -> Null+                [n] -> String n+                (email:name:_) -> String $ T.concat [name, " <", email, ">"]++-- | Query abook for the given string+getAbookQueryR :: Handler Value+getAbookQueryR = do+    q <- fromMaybe "" <$> lookupGetParam "q"+    let p = proc "abook" ["--mutt-query", T.unpack q]+    rs <- try $ runResourceT $ sourceProcess p $= C.decode C.utf8 $= C.lines $$ C.consume+    return $ case rs of+        Left (ExitFailure 1) -> Null -- exit code 1 is for no results+        Left e -> object [ "error" .= show e ]+        Right [] -> Null+        Right [_] -> Null -- first line is a blank line+        Right (_:qs) -> Array $ V.fromList $ map parseLine qs
src/Handler/Compose.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS_GHC -fno-warn-orphans #-} {- Copyright (C) 2013 John Lenz <lenz@math.uic.edu> @@ -14,236 +15,261 @@ You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>. -}-module Handler.Compose (getComposeR, postComposeR, getReplyR, getReplyAllR) where+module Handler.Compose (+    getComposeR+  , postComposeR+  , getReplyR+  , getReplyAllR+  , postPreviewMessageR+) where  import Import-import Settings-import Network.Mail.Mime+import Handler.ComposeFields++import Data.String (fromString) import Data.Time+import Network.Mail.Mime hiding (partContent)+import System.FilePath ((</>)) import System.Locale import System.Random (randomIO)-import System.FilePath ((</>))-import qualified Data.ByteString as B+import Text.Blaze.Html.Renderer.Utf8 (renderHtml)+import Text.Markdown+ import qualified Data.ByteString.Lazy as BL+import qualified Data.CaseInsensitive as CI import qualified Data.Conduit.List as CL+import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Text.Lazy.IO as TL-import qualified Text.Email.Validate as E-import qualified Data.Map as M-import qualified Data.CaseInsensitive as CI-import Data.Attoparsec.Text-import Data.String (fromString)-import NotmuchCmd --- | Parse an email address in angle brackets-emailInBrackets :: Parser T.Text-emailInBrackets = do-    void $ char '<'-    y <- takeTill (=='>') <?> "Email address"-    void (char '>' <?> "Expecting '>'")-    skipSpace-    return y+-----------------------------------------------------------------------------------------+-- Compose Form+----------------------------------------------------------------------------------------- --- | Parses an email address, which can either just be a direct email--- address or can be an address in the form "Name <email>"-address :: Parser Address-address = do-    x <- T.strip <$> takeTill (\c -> c == '<' || c == ',')-    y <- Just <$> emailInBrackets <|> return Nothing-    case y of-        Just e -> return $ Address (Just x) e-        Nothing -> return $ Address Nothing x+instance Eq Address where+    (Address a1 a2) == (Address b1 b2) = a1 == b1 && a2 == b2 --- | Parse a list of addresses seperated by commas-addresses :: Parser [Address]-addresses = do as <- sepBy address (char ',')-               endOfInput <?> "Expecting ',' or '>'"-               return as+-- | Lookup From addresses in the settings+fromAddresses :: (MonadHandler m, HandlerSite m ~ App) => m (OptionList Address)+fromAddresses = mkOptionList <$> do+    addrs <- extraFromAddresses <$> getExtra+    forM addrs $ \a ->+        case parseAddress a of+            Left err -> do+                setMessageI err+                return $ Option ("Invalid " <> a) (Address Nothing "") ""+            Right a' -> return $ Option a a' a --- | A version of parseOnly which includes the context of the failure.-parseOnly' :: Parser a -> T.Text -> Either String a-parseOnly' p t = checkRes (parse p t)-    where checkRes result = case result of-                              Fail _ ctx err -> Left $ show ctx ++ " " ++ err-                              Partial f -> checkRes $ f ""-                              Done _ x -> Right $ x+-- | Parse an address header like To: and CC: into a list of address+parseAddrHeader :: (MonadHandler m, HandlerSite m ~ App) => CI.CI T.Text -> Reply -> m [Address]+parseAddrHeader hdr reply = +    case M.lookup hdr (replyHeaders reply) of+        Nothing -> return []+        Just x  -> case parseAddresses x of+                           Left err -> do setMessageI err+                                          return []+                           Right addr -> return addr -parseAddresses :: T.Text -> Either (SomeMessage App) (Maybe [Address])-parseAddresses t = case parseOnly' addresses t of-                       Left err -> Left $ fromString err-                       Right [] -> Right $ Nothing-                       Right addrs -> Just <$> mapM checkAddr addrs-    where-        checkAddr a@(Address _ e) | E.isValid (T.encodeUtf8 e) = Right a-        checkAddr (Address _ e)   | otherwise = Left $ SomeMessage $ MsgInvalidEmail e+-- | Search for the first part which is a body+findBodyText :: [MessagePart] -> [T.Text]+findBodyText [] = []+findBodyText ((MessagePart {partContent = ContentText t}):_) = map (\x -> "> " <> x <> "\n") $ T.lines t+findBodyText ((MessagePart {partContent = ContentMsgRFC822 _}):_) = []+findBodyText ((MessagePart {partContent = ContentMultipart sub}):ms) =+    case findBodyText sub of+        [] -> findBodyText ms+        x  -> x -showAddresses :: [Address] -> T.Text-showAddresses as = T.intercalate ", " $ map showA as-    where showA (Address {addressName = Just name, addressEmail = e}) = T.concat [name, " <", e, ">"]-          showA (Address {addressName = Nothing, addressEmail = e}) = e+-- | Parse a reply into a mail message and the reply body+parseReply :: (MonadHandler m, HandlerSite m ~ App) => Reply -> m (Mail, T.Text)+parseReply reply = do+    to <- parseAddrHeader "to" reply+    cc <- parseAddrHeader "cc" reply -addressField :: Field (HandlerT App IO) [Address]-addressField = Field { fieldParse = \x _ -> case x of-                                                [] -> return $ Right Nothing-                                                ("":_) -> return $ Right Nothing-                                                (n:_) -> return $ parseAddresses n-                     , fieldView = \theId name attrs val isReq -> [whamlet|-<input id=#{theId} name=#{name} *{attrs} type=text :isReq:required value=#{either id showAddresses val}>-|]-                     , fieldEnctype = UrlEncoded-                     }+    let extra = foldr M.delete (replyHeaders reply) ["to", "cc", "subject", "from"] -header :: Parser (B.ByteString, T.Text)-header = do-    k <- takeWhile1 (\c -> not (isEndOfLine c) && c /= ':')-    void $ char ':'-    skipSpace-    v <- takeTill isEndOfLine-    return (T.encodeUtf8 k, v)+    let mail = Mail (Address Nothing "")+                    to+                    cc+                    [] -- bcc+                    [(T.encodeUtf8 $ CI.original k, v) | (k,v) <- M.toList extra]+                    [] -- parts -headers :: Parser [(B.ByteString, T.Text)]-headers = sepBy header endOfLine <?> "Headers"+    tz <- liftIO getCurrentTimeZone+    let t = utcToZonedTime tz $ messageTime $ replyOriginal reply+    let ts = formatTime defaultTimeLocale "%a %b %e %R %z %Y" t -parseHeaders :: T.Text -> Either (SomeMessage App) (Maybe [(B.ByteString,T.Text)])-parseHeaders t = case parseOnly' headers t of-                     Left err -> Left $ fromString err-                     Right [] -> Right $ Nothing-                     Right h -> Right $ Just h+    let body = T.concat $+                [ "On "+                , T.pack ts+                , ", "+                , fromMaybe "" $ M.lookup "from" $ messageHeaders $ replyOriginal reply+                , " wrote:\n"+                ] ++ (findBodyText $ messageBody $ replyOriginal reply) -showHeaders :: [(B.ByteString, T.Text)] -> T.Text-showHeaders hs = T.intercalate "\n" $ map showH hs-    where showH (x,y) = T.concat [T.decodeUtf8 x, ": ", y]+    return (mail, body) -headerField :: Field (HandlerT App IO) [(B.ByteString,T.Text)]-headerField = Field { fieldParse = \x _ -> case x of-                                                [] -> return $ Right Nothing-                                                ("":_) -> return $ Right Nothing-                                                (n:_) -> return $ parseHeaders n-                     , fieldView = \theId name attrs val isReq -> [whamlet|-<textarea id=#{theId} name=#{name} *{attrs} rows=4 cols=50 wrap=off :isReq:required>-    #{either id showHeaders val}-|]-                     , fieldEnctype = UrlEncoded-                     }+data EmailBodyFormat = EmailBodyQuotedPrintable+                     | EmailBodyPlain+                     | EmailBodyMarkdown+  deriving (Eq, Enum, Bounded) -multiFile :: Field (HandlerT master IO) [FileInfo]-multiFile = Field p view Multipart-    where-        p _ fs = return $ Right $ Just fs-        view fId name attrs _ _ = [whamlet|-<input type=file name=#{name} ##{fId} multiple *{attrs}>-|]+instance Show EmailBodyFormat where+    show EmailBodyQuotedPrintable = "Send as text/plain, UTF-8 encoded with quoted printable"+    show EmailBodyPlain = "Send as text/plain, UTF-8, no encoding"+    show EmailBodyMarkdown = "Parse body as markdown; send text and html parts" -findBodyText :: [MessagePart] -> Maybe T.Text-findBodyText [] = Nothing-findBodyText ((MessagePart {NotmuchCmd.partContent = ContentText t}):_) = Just t-findBodyText ((MessagePart {NotmuchCmd.partContent = ContentMsgRFC822 _}):_) = Nothing-findBodyText ((MessagePart {NotmuchCmd.partContent = ContentMultipart sub}):ms) =-    case findBodyText sub of-        Nothing -> findBodyText ms-        Just x -> Just x+markdownSettings :: MarkdownSettings+markdownSettings = def { msXssProtect = False -- Input is trusted from compose form+                       , msBlankBeforeBlockquote = False+                       } -replyBody :: Reply -> T.Text-replyBody (Reply {replyOriginal = m})  = T.concat [onmsg, "\n", bdy]-  where-    origfrom = maybe "" id $ M.lookup "from" $ messageHeaders m-    onmsg = T.concat ["On ", T.pack (show $ messageTime m), ", ", origfrom, " wrote: "]-    origlines = T.lines $ maybe "" id $ findBodyText $ messageBody m-    addquote = map (T.append "> ") origlines-    bdy = T.unlines addquote+-- | Read the body as markdown and create html+markdownToHtml :: T.Text -> Html+markdownToHtml = markdown markdownSettings . TL.fromStrict -fileToAttach :: FileInfo -> ResourceT IO Alternatives-fileToAttach f = do-    content <- fileSource f $$ CL.consume-    return [Part (fileContentType f) Base64 (Just $ fileName f) [] (BL.fromChunks content)]+-- | Create the body of the outgoing message+createBody :: (MonadHandler m, HandlerSite m ~ App)+           => EmailBodyFormat -> Textarea -> [FileInfo] -> m [Alternatives]+createBody fmt bodytext attach = do+    attachParts <- liftResourceT $ forM attach $ \f -> do+        content <- fileSource f $$ CL.consume+        return [Part (fileContentType f) Base64 (Just $ fileName f) [] (BL.fromChunks content)] -composeForm :: Address -> Maybe Reply -> Form Mail-composeForm from mreply fmsg = do-    let mheaders = replyHeaders <$> mreply-        mto = M.lookup "to" =<< mheaders-        mtoaddr = either (const Nothing) id . parseAddresses =<< mto-        mcc = M.lookup "cc" =<< mheaders-        mccaddr = either (const Nothing) Just . parseAddresses =<< mcc-        --from = maybe (Just f) $ M.lookup "from" =<< extraheaders-        msubj = M.lookup "subject" =<< mheaders-        mbody = Textarea . replyBody <$> mreply-        mextramap = M.delete "to" . M.delete "cc" . M.delete "subject" . M.delete "from" <$> mheaders-        mextra = (\hs -> Just [ (T.encodeUtf8 $ CI.original k, v) | (k,v) <- M.toList hs ]) <$> mextramap+    let b = Part "text/plain; charset=UTF-8" None Nothing [] $ BL.fromChunks [T.encodeUtf8 $ unTextarea bodytext]+        bq = b { partEncoding = QuotedPrintableText }+        html = renderHtml $ markdownToHtml $ unTextarea bodytext+        hpart = Part "text/html; charset=UTF-8" QuotedPrintableText Nothing [] html -    (to,toView) <- mreq addressField (FieldSettings "To" Nothing (Just "to") Nothing []) mtoaddr-    (cc,ccView) <- mopt addressField (FieldSettings "CC" Nothing (Just "cc") Nothing []) mccaddr-    (bcc,bccView) <- mopt addressField (FieldSettings "BCC" Nothing (Just "bcc") Nothing []) Nothing-    (subject,sView) <- mreq textField (FieldSettings (SomeMessage MsgSubject) Nothing (Just "subject") Nothing []) msubj-    (head,hView) <- mopt headerField (FieldSettings (SomeMessage MsgExtraHeader) Nothing (Just "extraheaders") Nothing []) mextra-    (body,bView) <- mreq textareaField (FieldSettings "Body" Nothing (Just "body") Nothing []) mbody-    (attach,attachView) <- mopt multiFile (FieldSettings (SomeMessage MsgAttach) Nothing (Just "attach") Nothing []) Nothing+    let body = case fmt of+                EmailBodyQuotedPrintable -> [bq]+                EmailBodyPlain -> [b]+                EmailBodyMarkdown -> [bq, hpart] -    attachParts <- case attach of-                       FormSuccess (Just xs) -> liftResourceT $ mapM fileToAttach xs-                       _ -> return []+    return $ body : attachParts -    let mkParts b = [[Part "text/plain" QuotedPrintableText Nothing [] $ BL.fromChunks [T.encodeUtf8 $ unTextarea b]]] ++ attachParts-        mkHeaders s e = ("Subject", s) : maybe [] id e-        mail = Mail <$> pure from+-- | Create a new message ID+messageID :: (MonadHandler m, HandlerSite m ~ App) => m T.Text+messageID = do t <- liftIO getCurrentTime+               let ts = formatTime defaultTimeLocale "%s" t+               i <- abs <$> liftIO (randomIO :: IO Int)+               domain <- extraMessageIDDomain <$> getExtra+               case domain of+                 "" -> return ""+                 _  -> return $ T.concat ["<notmuch-web-", T.pack ts, ".", T.pack (show i), "@", domain, ">"]++-- | Create a field settings from a string.  The default IsString instance does not set the id.+fStr :: T.Text -> FieldSettings site+fStr i = FieldSettings (fromString $ T.unpack i) Nothing (Just i) Nothing []++-- | Create a field setting from a message+fI :: AppMessage -> T.Text -> FieldSettings App+fI m i = FieldSettings (SomeMessage m) Nothing (Just i) Nothing []++-- | A helper widget to display a form element in bootstrap markup+formElem :: Bool -> FieldView App -> Widget+formElem includeHelp v = [whamlet|+<div .control-group>+    <label .control-label for=#{fvId v}>#{fvLabel v}+    <div .controls>+        ^{fvInput v}+        $if includeHelp+            <i .addr-help .icon-question-sign data-title=_{MsgAddrHelpTitle} data-content=_{MsgAddrHelp}>+|]++-- | A widget to select the address book type+addressBookWidget :: Widget+addressBookWidget = do+    $(widgetFile "abook")+    mID <- extraGoogleClientId <$> getExtra+    case mID of+        Nothing -> $(widgetFile "plain-contacts")+        Just clientID -> $(widgetFile "google-contacts")++-- | The compose form+composeForm :: Maybe Reply -> Form Mail+composeForm mreply fmsg = do+    mmail <- lift $ maybe (return Nothing) (\x -> Just <$> parseReply x) mreply++    (from,fromView) <- mreq (selectField fromAddresses) (fStr "From") Nothing+    (to,toView) <- mreq addressField (fStr "To") (mailTo . fst <$> mmail)+    (cc,ccView) <- mopt addressField (fStr "CC") (Just . mailCc . fst <$> mmail)+    (bcc,bccView) <- mopt addressField (fStr "BCC") Nothing+    (subject,sView) <- mreq textField (fI MsgSubject "subj") $ M.lookup "subject" =<< (replyHeaders <$> mreply)+    (head,hView) <- mopt headerField (fI MsgExtraHeader "hdrs") (Just . mailHeaders . fst <$> mmail)+    (bfmt,fmtView) <- mreq (selectField optionsEnum) (fI MsgBodyFormat "bdyfmt") Nothing+    (body,bView) <- mreq textareaField (fI MsgBody "Body") (Textarea . snd <$> mmail)+    (attach,attachView) <- mopt multiFile (fI MsgAttach "attch") Nothing++    parts <- case (,,) <$> bfmt <*> body <*> attach of+               FormSuccess (f,b,a) -> FormSuccess <$> createBody f b (fromMaybe [] a)+               FormFailure err     -> return $ FormFailure err+               FormMissing         -> return $ FormMissing+               +    mid <- lift messageID++    let mkHeaders s e = ("Subject", s) : ("Message-ID", mid) : fromMaybe [] e+        mail = Mail <$> from                     <*> to-                    <*> (maybe [] id <$> cc)-                    <*> (maybe [] id <$> bcc)+                    <*> (fromMaybe [] <$> cc)+                    <*> (fromMaybe [] <$> bcc)                     <*> (mkHeaders <$> subject <*> head)-                    <*> (mkParts <$> body)+                    <*> parts -        widget = [whamlet|- #{fmsg}- $forall v <- [toView, ccView, bccView, sView, hView]-    <div .control-group .span10>-        <label .control-label for=#{fvId v}>#{fvLabel v}-        <div .controls>-            ^{fvInput v}- <div .control-group>-    ^{fvInput bView}- <div .control-group>-    <label .control-label for=#{fvId attachView}>#{fvLabel attachView}-    <div .controls>-        ^{fvInput attachView}+        widget = [whamlet|#{fmsg}+<div .row>+    <div .span6>+        ^{formElem False fromView}+        ^{formElem True toView}+        ^{formElem False ccView}+        ^{formElem False bccView}+    <div .span6>+        ^{addressBookWidget}+        $forall v <- [hView, fmtView]+            ^{formElem False v}+^{formElem False sView}+^{formElem False bView}+^{formElem False attachView} |]     return (mail, widget) --- | Lookup the configured from address-fromAddress :: Handler Address-fromAddress = do-    from <- extraFromAddress <$> getExtra-    case parseAddresses from of-        Left err -> invalidArgsI [err]-        Right (Just [x]) -> return x-        Right _ -> invalidArgs ["From address is invalid"]+previewForm :: Form T.Text+previewForm = renderDivs $ areq textField (fStr "previewtext") Nothing -getComposeR :: Handler RepHtml+-----------------------------------------------------------------------------------------+-- Handlers+-----------------------------------------------------------------------------------------++getComposeR :: Handler Html getComposeR = do-    from <- fromAddress-    ((_,widget),enctype) <- runFormPost $ composeForm from Nothing+    ((_,widget),enctype) <- runFormPost $ composeForm Nothing+    (previewWidget,previewEnctype) <- generateFormPost previewForm     defaultLayout $ do         setTitleI MsgCompose         let err = [] :: [String]         $(widgetFile "compose") -replyHandler :: ReplyTo -> MessageID -> Handler RepHtml+-- | Helper function for reply and reply all+replyHandler :: ReplyTo -> MessageID -> Handler Html replyHandler rto m = do-    from <- fromAddress     reply <- notmuchReply m rto-    ((_,widget),enctype) <- runFormPost $ composeForm from $ Just reply+    ((_,widget),enctype) <- runFormPost $ composeForm $ Just reply+    (previewWidget,previewEnctype) <- generateFormPost previewForm     defaultLayout $ do         setTitleI MsgCompose         let err = [] :: [String]         $(widgetFile "compose") -getReplyR :: MessageID -> Handler RepHtml+getReplyR :: MessageID -> Handler Html getReplyR = replyHandler ReplySender -getReplyAllR :: MessageID -> Handler RepHtml+getReplyAllR :: MessageID -> Handler Html getReplyAllR = replyHandler ReplyAll +-- | Create a unique filename and a date header from the current time filenameAndDate :: IO (FilePath, TL.Text) filenameAndDate = do t <- getCurrentTime >>= utcToLocalZonedTime                      let ts = formatTime defaultTimeLocale "%F-%T%z" t@@ -251,23 +277,13 @@                      i <- randomIO :: IO Int                      return (ts ++ "-" ++ show i, "Date: " <> TL.pack ds <> "\n") -messageID :: Handler T.Text-messageID = do t <- liftIO getCurrentTime-               let ts = formatTime defaultTimeLocale "%s" t-               i <- abs <$> liftIO (randomIO :: IO Int)-               domain <- extraMessageIDDomain <$> getExtra-               case domain of-                 "" -> return ""-                 _  -> return $ T.concat ["<notmuch-web-", T.pack ts, ".", T.pack (show i), "@", domain, ">"]--postComposeR :: Handler RepHtml+postComposeR :: Handler Html postComposeR = do-    from <- fromAddress-    ((result,widget),enctype) <- runFormPost $ composeForm from Nothing+    ((result,widget),enctype) <- runFormPost $ composeForm Nothing+    (previewWidget,previewEnctype) <- generateFormPost previewForm     case result of         FormSuccess m -> do-            mid <- messageID-            msg <- liftIO $ renderMail' $ m {mailHeaders = mailHeaders m ++ [("Message-ID", mid)]}+            msg <- liftIO $ renderMail' m             let tmsg = TL.decodeUtf8 msg              when production $ do@@ -286,3 +302,11 @@         FormFailure err -> defaultLayout $ do             setTitleI MsgCompose             $(widgetFile "compose")++postPreviewMessageR :: Handler Html+postPreviewMessageR = do+    ((result,_),_) <- runFormPost previewForm+    case result of+        FormMissing -> invalidArgs ["Form is missing"]+        FormFailure err -> invalidArgs err+        FormSuccess h -> return $ markdownToHtml h
+ src/Handler/ComposeFields.hs view
@@ -0,0 +1,168 @@+{-+Copyright (C) 2013 John Lenz <lenz@math.uic.edu>++This program is free software: you can redistribute it and/or modify+it under the terms of the GNU Affero General Public License as published by+the Free Software Foundation, either version 3 of the License, or+(at your option) any later version.++This program is distributed in the hope that it will be useful,+but WITHOUT ANY WARRANTY; without even the implied warranty of+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+GNU Affero General Public License for more details.++You should have received a copy of the GNU Affero General Public License+along with this program.  If not, see <http://www.gnu.org/licenses/>.+-}+module Handler.ComposeFields (+    addressField+  , parseAddress+  , parseAddresses+  , showAddress+  , headerField+  , multiFile+) where++import Import+import StaticFiles++import Data.Attoparsec.Text+import Data.String (fromString)+import Network.Mail.Mime (Address(..))++import qualified Data.ByteString as B+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Text.Email.Validate as E++-- | Parse an email address in angle brackets+emailInBrackets :: Parser T.Text+emailInBrackets = do+    void $ char '<'+    y <- takeTill (=='>') <?> "Email address"+    void (char '>' <?> "Expecting '>'")+    skipSpace+    return y++quotedName :: Parser T.Text+quotedName = do+    void $ char '"'+    x <- takeTill (=='"')+    void $ char '"'+    skipSpace+    let parts = T.split (==',') x+    return $ case parts of+                (p:ps) -> T.unwords $ map T.strip $ ps ++ [p]+                [] -> ""++unquotedName :: Parser T.Text+unquotedName = T.strip <$> takeTill (\x -> x == '<' || x == ',')++-- | Parses an email address, which can either just be a direct email+-- address or can be an address in the form "Name <email>"+address :: Parser Address+address = do+    skipSpace+    x <- quotedName <|> unquotedName+    y <- Just <$> emailInBrackets <|> return Nothing+    case y of+        Just e -> return $ Address (Just x) e+        Nothing -> return $ Address Nothing x++-- | Parse a list of addresses seperated by commas+addresses :: Parser [Address]+addresses = do as <- address `sepBy1` char ','+               endOfInput <?> "Expecting ',' or '>'"+               return as++-- | Checks if an email is valid+checkAddr :: Address -> Either (SomeMessage App) Address+checkAddr a@(Address _ e) | E.isValid (T.encodeUtf8 e) = Right a+checkAddr (Address _ e) = Left $ SomeMessage $ MsgInvalidEmail e++-- | Parse a single address+parseAddress :: T.Text -> Either (SomeMessage App) Address+parseAddress t = case parseOnly' address t of+                       Left err -> Left $ fromString $ concat ["Error parsing ", T.unpack t, ": ", err]+                       Right a  -> checkAddr a++-- | Parse a list of addresses separated by commas+parseAddresses :: T.Text -> Either (SomeMessage App) [Address]+parseAddresses t = case parseOnly' addresses t of+                       Left err -> Left $ fromString $ concat ["Error parsing ", T.unpack t, ": ", err]+                       Right [(Address Nothing "")] -> Right []+                       Right a -> mapM checkAddr a++showAddress :: Address -> T.Text+showAddress (Address {addressName = Just name, addressEmail = e}) = T.concat [name, " <", e, ">"]+showAddress (Address {addressName = Nothing, addressEmail = e}) = e++addrWidget :: FieldViewFunc (HandlerT App IO) [Address]+addrWidget theID name attrs val isReq = do +    addStylesheet $ StaticR css_select2_css+    addScript $ StaticR js_select2_3_4_1_min_js++    let addrs = either id (T.intercalate "," . map showAddress) val++    [whamlet|+      <input type=text ##{theID} name=#{name} .address-field :isReq:required value="#{addrs}" *{attrs}>+    |]++addressField :: Field (HandlerT App IO) [Address]+addressField = Field+  { fieldParse = \addr _ -> case addr of+                              []    -> return $ Right Nothing+                              (a:_) -> return $ Just <$> parseAddresses a+  , fieldView = addrWidget+  , fieldEnctype = UrlEncoded+  }++-- | Parse a header+header :: Parser (B.ByteString, T.Text)+header = do+    k <- takeWhile1 (\c -> not (isEndOfLine c) && c /= ':')+    void $ char ':'+    skipSpace+    v <- takeTill isEndOfLine+    return (T.encodeUtf8 k, v)++-- | Parse a list of headers+headers :: Parser [(B.ByteString, T.Text)]+headers = sepBy header endOfLine <?> "Headers"++headerField :: Field (HandlerT App IO) [(B.ByteString,T.Text)]+headerField = Field +    { fieldParse = \x _ -> case x of+                             [] -> return $ Right Nothing+                             ("":_) -> return $ Right Nothing+                             (n:_) -> return $ case parseOnly' headers n of+                                                 Left err -> Left $ fromString err+                                                 Right [] -> Right $ Nothing+                                                 Right h  -> Right $ Just h+    , fieldView = \theId name attrs val isReq -> do+        let hdrs = case val of+                     Left txt -> txt+                     Right vals -> T.intercalate "\n" [ T.decodeUtf8 x <> ": " <> y | (x,y) <- vals]+        [whamlet|+<textarea id=#{theId} name=#{name} *{attrs} rows=4 cols=50 wrap=off :isReq:required>+    #{hdrs}+|]+    , fieldEnctype = UrlEncoded+    }++multiFile :: Field (HandlerT master IO) [FileInfo]+multiFile = Field p view Multipart+    where+        p _ fs = return $ Right $ Just fs+        view fId name attrs _ _ = [whamlet|+<input type=file name=#{name} ##{fId} multiple *{attrs}>+|]+++-- | A version of parseOnly which includes the context of the failure.+parseOnly' :: Parser a -> T.Text -> Either String a+parseOnly' p t = checkRes (parse p t)+    where checkRes result = case result of+                              Fail _ ctx err -> Left $ show ctx ++ " " ++ err+                              Partial f -> checkRes $ f ""+                              Done _ x -> Right x
src/Handler/Home.hs view
@@ -23,13 +23,11 @@   ) where  import Import-import Settings+import Handler.Tags+ import Text.Hamlet (hamletFile) import qualified Data.Text as T -import Handler.Tags-import NotmuchCmd- searchTable :: String -> Widget searchTable s = do     tagHeader@@ -37,7 +35,7 @@     $(widgetFile "search")  -- | The GET handler for the home page-getHomeR :: Handler RepHtml+getHomeR :: Handler Html getHomeR = defaultLayout $ do     folders <- extraFolders <$> getExtra     case folders of@@ -46,7 +44,7 @@         _ -> setTitle "Notmuch"  -- | The GET handler for the search page-getSearchR :: String -> Handler RepHtml+getSearchR :: String -> Handler Html getSearchR s = defaultLayout $ do     setTitle $ toHtml $ "Notmuch - " ++ s     searchTable s
src/Handler/Pager.hs view
@@ -18,16 +18,13 @@ module Handler.Pager (getThreadPagerR) where  import Import-import Prelude (tail)-import Settings import StaticFiles-import NotmuchCmd-import Data.Maybe (listToMaybe) import Handler.View++import Data.Aeson (encode)+import Text.Blaze (unsafeLazyByteString) import qualified Data.Text as T import qualified Data.HashMap.Strict as H-import Text.Blaze (unsafeLazyByteString)-import Data.Aeson (encode)  encodeSearch :: (Route App -> T.Text) -> [SearchResult] -> Html encodeSearch ur search = unsafeLazyByteString $ encode $ map enc search@@ -37,10 +34,10 @@                   Object o -> o                   _ -> error "Search is not an object" -getThreadPagerR :: String -> Handler RepHtml+getThreadPagerR :: String -> Handler Html getThreadPagerR s = defaultLayout $ do     search <- notmuchSearch s     ur <- getUrlRender     retags <- extraRetag <$> getExtra-    addScript $ StaticR js_jquery_pjax_1_7_0_js+    addScript $ StaticR js_jquery_pjax_1_7_3_js     $(widgetFile "pager")
src/Handler/Raw.hs view
@@ -17,14 +17,13 @@ module Handler.Raw (getRawCommandR, postRawCommandR) where  import Import-import NotmuchCmd import qualified Data.Text as T  rawForm :: AForm (HandlerT App IO) String rawForm = T.unpack <$> areq textField cmd Nothing     where cmd = FieldSettings (SomeMessage MsgRaw) Nothing (Just "Command") Nothing [] -getRawCommandR :: Handler RepHtml+getRawCommandR :: Handler Html getRawCommandR = defaultLayout $ do     setTitleI MsgRaw     ((result,widget),enctype) <- liftHandlerT $ runFormPost $ renderBootstrap rawForm@@ -41,7 +40,7 @@     <input .btn .primary type=submit value=_{MsgRunCmd}> |] -postRawCommandR :: Handler RepHtml+postRawCommandR :: Handler Html postRawCommandR = do     ((result,_),_) <- runFormPost $ renderBootstrap rawForm     command <- case result of
src/Handler/Tags.hs view
@@ -24,14 +24,20 @@ ) where  import Import-import Settings+--import Data.Hashable (hash) import qualified Data.Text as T -import NotmuchCmd- displayTag :: T.Text -> WidgetT App IO () displayTag "unread" = [whamlet|<span .label .label-important .label-tag>unread|]-displayTag t = [whamlet|<span .label .label-info .label-tag>#{t}|]+displayTag t        = [whamlet|<span .label .label-info .label-tag>#{t}|]+{-+displayTag t = case hash t `mod` 5 of+                0 -> [whamlet|<span .label .label-success .label-tag>#{t}|]+                1 -> [whamlet|<span .label .label-tag>#{t}|]+                2 -> [whamlet|<span .label .label-warning .label-tag>#{t}|]+                3 -> [whamlet|<span .label .label-inverse .label-tag>#{t}|]+                _ -> [whamlet|<span .label .label-info .label-tag>#{t}|]+-}  -- | A widget consisting of the retag buttons followed by the current tag list. tagWidget :: Either SearchResult Message -> WidgetT App IO ()@@ -48,7 +54,7 @@   retags <- extraRetag <$> getExtra   [whamlet| <span .tags>-    <div .btn-group>+    <span .btn-group .hide-noscript>         $forall r <- retags             <button .btn .retag-button .btn-link title="#{retagName r}" data-notmuch-url="@{url (retagName r)}">                 <i class="#{retagIcon r}">@@ -79,7 +85,7 @@     (customWidget,customEnc) <- liftHandlerT $ generateFormPost customRetagForm     $(widgetFile "tag-header") -postRetagThreadR :: ThreadID -> T.Text -> Handler RepJson+postRetagThreadR :: ThreadID -> T.Text -> Handler Value postRetagThreadR t name = do     ((result,_),_) <- runFormPost retagForm     case result of@@ -92,9 +98,9 @@     case retag of         [] -> notFound         (r:_) -> do notmuchTagThread (retagAdd r) (retagRemove r) t-                    return $ repJson $ toJSON r+                    return $ toJSON r -postRetagMessageR :: MessageID -> T.Text -> Handler RepJson+postRetagMessageR :: MessageID -> T.Text -> Handler Value postRetagMessageR m name = do     ((result,_),_) <- runFormPost retagForm     case result of@@ -107,9 +113,9 @@     case retag of         [] -> notFound         (r:_) -> do notmuchTagMessage (retagAdd r) (retagRemove r) m-                    return $ repJson $ toJSON r+                    return $ toJSON r -postCustomRetagThreadR :: ThreadID -> Handler RepJson+postCustomRetagThreadR :: ThreadID -> Handler Value postCustomRetagThreadR t = do     ((result,_),_) <- runFormPost customRetagForm     (add, remove) <- case result of@@ -120,9 +126,9 @@         rem' = maybe [] words remove      notmuchTagThread add' rem' t-    return $ repJson $ object [ "add" .= add', "remove" .= rem' ]+    return $ object [ "add" .= add', "remove" .= rem' ] -postCustomRetagMessageR :: MessageID -> Handler RepJson+postCustomRetagMessageR :: MessageID -> Handler Value postCustomRetagMessageR m = do     ((result,_),_) <- runFormPost customRetagForm     (add, remove) <- case result of@@ -133,4 +139,4 @@         rem' = maybe [] words remove      notmuchTagMessage add' rem' m-    return $ repJson $ object [ "add" .= add', "remove" .= rem' ]+    return $ object [ "add" .= add', "remove" .= rem' ]
src/Handler/View.hs view
@@ -23,52 +23,56 @@ ) where  import Import-import Blaze.ByteString.Builder (fromByteString)-import Control.Monad (replicateM, unless)-import Data.List (find) import FilterHtml+import StaticFiles import Handler.Tags++import Blaze.ByteString.Builder (fromByteString) import Network.HTTP.Types (status200) import Network.Wai (Response(..))-import NotmuchCmd-import Settings-import StaticFiles-import Text.Blaze.Html5 (preEscapedToHtml)-import Text.Pandoc+import Text.Blaze.Html.Renderer.Text (renderHtml)+import Text.Markdown import qualified Data.ByteString as B-import qualified Data.Conduit as C+import qualified Data.CaseInsensitive as CI import qualified Data.Conduit.List as C import qualified Data.Conduit.Text as C+import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as TL-import qualified Data.Text.ICU.Convert as ICU-import qualified Data.Map as M-import qualified Data.Set as S import qualified Data.Tree as TR-import qualified Data.CaseInsensitive as CI +#ifdef USE_ICU+import qualified Data.Text.ICU.Convert as ICU+#endif+ decodePart :: (MonadLogger m, MonadHandler m) -           => Maybe T.Text -> C.Source (C.ResourceT IO) B.ByteString -> m TL.Text+           => Maybe (CI.CI T.Text) -> Source (ResourceT IO) B.ByteString -> m TL.Text decodePart charset src = case charset of                             Just "ISO-8859-1" -> decodeConduit C.iso8859_1                             Just "UTF-8"      -> decodeConduit C.utf8-                            Just x            -> decodeICU x-                            Nothing           -> decodeConduit C.utf8+#ifdef USE_ICU+                            Just x            -> decodeICU $ CI.original x+#endif+                            _                 -> decodeConduit C.utf8+   where decodeConduit c = TL.fromChunks <$> liftResourceT (src $= C.decode c $$ C.consume)++#ifdef USE_ICU         decodeICU x = do $(logInfo) ("Decoding using ICU: " `T.append` x)                          raw <- liftResourceT (src $$ C.consume)                          c <- liftIO $ ICU.open (T.unpack x) (Just True)-                         return $ TL.fromChunks [ICU.toUnicode c $ B.concat raw]+                         return $ TL.fromStrict $ ICU.toUnicode c $ B.concat raw+#endif  messagePart :: MessageID -> Bool -> MessagePart -> Widget  messagePart mid _ p@(MessagePart {partContentType = "text/html"}) = do     let ((_ :: IO MessagePart), src) = notmuchMessagePart mid $ partID p-    html <- decodePart (partContentCharset p) src+    html <- TL.toStrict <$> decodePart (partContentCharset p) src     [whamlet| <div .message-part .message-html>-    #{preEscapedToHtml $ filterHtml html}+    #{preEscapedToMarkup $ filterHtml html} |]  messagePart mid _ m@(MessagePart {partContent = ContentText ""}) = [whamlet|@@ -80,7 +84,7 @@               #{f}           $of Nothing               <span>No filename-    (#{partContentType m})+    (#{CI.original $ partContentType m}) |]  -- Text which is part of an alternative@@ -92,7 +96,7 @@  -- Text not part of an alternative messagePart _ False (MessagePart {partContent = ContentText txt}) = do-    let html = TL.pack $ writeHtmlString pandocWriterOpts $ readMarkdown pandocReaderOpts $ T.unpack txt+    let html = TL.toStrict $ renderHtml $ markdown markdownSettings $ TL.fromStrict txt     htmlId <- newIdent     txtId <- newIdent     [whamlet|@@ -104,7 +108,7 @@             <a data-toggle=tab data-target="##{txtId}">Text     <div .tab-content>         <div .tab-pane ##{htmlId}>-            #{preEscapedToHtml $ filterHtml html}+            #{preEscapedToMarkup $ filterHtml html}         <div .tab-pane .active ##{txtId}>             <pre>                 #{txt}@@ -123,7 +127,7 @@     <ul .nav .nav-tabs>         $forall (i,p) <- zip ids alternatives             <li :isActive p:.active>-                <a data-toggle=tab data-target="##{i}">#{partContentType p}+                <a data-toggle=tab data-target="##{i}">#{CI.original $ partContentType p}     <div .tab-content>         $forall (i,p) <- zip ids alternatives             <div .tab-pane :isActive p:.active ##{i}>@@ -194,7 +198,7 @@     tagHeader     $(widgetFile "thread-header") -getThreadR :: ThreadID -> Handler RepHtml+getThreadR :: ThreadID -> Handler Html getThreadR t = defaultLayout $ do     pjax <- isPjax     unless pjax threadHeader@@ -208,22 +212,14 @@     let contentdisp = case partContentFilename msg of                         Just f  -> [("Content-Disposition", "attachment;filename=\"" <> T.encodeUtf8 f <> "\"")]                         Nothing -> []-    let contenttype = [("Content-Type", T.encodeUtf8 $ partContentType msg)]+    let contenttype = [("Content-Type", T.encodeUtf8 $ CI.original $ partContentType msg)]      let source = rawMsg $= C.map (Chunk . fromByteString)      sendWaiResponse $ ResponseSource status200 (contentdisp ++ contenttype) source --pandocWriterOpts :: WriterOptions-pandocWriterOpts = def-    { writerHtml5    = True-    , writerWrapText = False-    }--pandocReaderOpts :: ReaderOptions-pandocReaderOpts = def-    { readerExtensions = S.delete Ext_blank_before_blockquote $ readerExtensions def-    , readerSmart      = True-    , readerParseRaw   = False-    }+markdownSettings :: MarkdownSettings+markdownSettings = def { msLinkNewTab = True+                       , msXssProtect = False -- We protect using our own filtering in FilterHtml.hs+                       , msBlankBeforeBlockquote = False+                       }
src/Import.hs view
@@ -16,29 +16,34 @@ -} module Import     ( module Prelude-    , module Foundation-    , module Settings-    , module Data.Conduit-    , module Data.Monoid     , module Control.Applicative     , module Control.Exception.Lifted     , module Control.Monad+    , module Data.Conduit+    , module Data.List+    , module Data.Maybe+    , module Data.Monoid+    , module Foundation+    , module NotmuchCmd+    , module Settings     , module Yesod     ) where  #if __GLASGOW_HASKELL__ >= 706-import Prelude hiding (writeFile, readFile, head, tail, init, last)+import Prelude hiding (writeFile, readFile, head, init, last) #else-import Prelude hiding (writeFile, readFile, head, tail, init, last, catch)+import Prelude hiding (writeFile, readFile, head, init, last, catch) #endif  import Foundation-import Settings (development, production)+import Settings+import NotmuchCmd -import Data.Conduit-import Data.Monoid (Monoid (mappend, mempty, mconcat), (<>)) import Control.Applicative ((<$>), (<*>), pure, (<|>)) import Control.Exception.Lifted (catch)-import Control.Monad (void, when)--import Yesod+import Control.Monad (void, when, forM, replicateM, unless)+import Data.Conduit+import Data.List (find)+import Data.Monoid (Monoid (mappend, mempty, mconcat), (<>))+import Data.Maybe (listToMaybe, fromMaybe, catMaybes)+import Yesod hiding (loadConfig)
src/NotmuchCmd.hs view
@@ -47,6 +47,7 @@   -- * Utils   , notmuchRaw   , notmuchJson+  , notmuchVersion ) where  import Prelude@@ -59,11 +60,14 @@ import Data.Conduit import Data.Conduit.Attoparsec (sinkParser) import Data.Conduit.Process+import Data.Monoid ((<>)) import Data.Time.Calendar (Day(..)) import Data.Time.Clock import Data.Time.Clock.POSIX import Data.Typeable (Typeable)+import Data.Version (Version(..), parseVersion) import Text.Blaze (ToMarkup(..))+import Text.ParserCombinators.ReadP (readP_to_S) import System.Process import System.Exit import Yesod (PathPiece)@@ -133,8 +137,8 @@  data MessagePart = MessagePart {       partID :: Int-    , partContentType :: T.Text-    , partContentCharset :: Maybe T.Text+    , partContentType :: CI.CI T.Text+    , partContentCharset :: Maybe (CI.CI T.Text)     , partContentFilename :: Maybe T.Text     , partContent :: MessageContent } deriving (Show,Eq)@@ -150,16 +154,16 @@ instance FromJSON MessagePart where     parseJSON (Object v) = do         i <- v .: "id"-        t <- v .: "content-type"+        t <- CI.mk . T.toLower <$> v .: "content-type"         x <- v .:? "content"         f <- v .:? "filename"-        cs <- v .:? "content-charset"-        let ctype = T.takeWhile (/= '/') t+        cs <- fmap CI.mk <$> v .:? "content-charset"+        let ctype = CI.map (T.takeWhile (/= '/')) t         case (ctype, x) of             ("multipart", Just (Array _)) -> MessagePart i t cs f . ContentMultipart <$> v .: "content"             ("message", Just (Array lst)) | t == "message/rfc822" -> MessagePart i t cs f <$> parseRFC822 lst             (_, Just (String c)) -> return $ MessagePart i t cs f $ ContentText c-            (_, Just _) -> return $ MessagePart i t cs f $ ContentText $ "Unknown content-type: " `T.append` t+            (_, Just _) -> return $ MessagePart i t cs f $ ContentText $ "Unknown content-type: " <> CI.original t             (_, Nothing) -> return $ MessagePart i t cs f $ ContentText ""      parseJSON x = fail $ "Error parsing part: " ++ show x@@ -286,3 +290,13 @@     case fromJSON v of         Error e -> throw $ NotmuchError $ "Error parsing for " ++ show args ++ " : " ++ e         Success x -> return x++-- | The version of notmuch+notmuchVersion :: MonadIO m => m Version+notmuchVersion = do+    out <- liftIO $ readProcess "notmuch" ["--version"] ""+    let vStr = words out !! 1+    let vs = filter (\(_,r) -> r == "") $ readP_to_S parseVersion vStr+    case vs of+        ((v,_):_) -> return v+        _ -> throw $ NotmuchError $ "Unable to parse version: " ++ vStr
src/Settings.hs view
@@ -15,52 +15,23 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>. -} --- | Settings are centralized, as much as possible, into this file. This--- includes database connection settings, static file locations, etc.--- In addition, you can configure a number of different aspects of Yesod--- by overriding methods in the Yesod typeclass. That instance is--- declared in the Foundation.hs file. module Settings where  import Prelude++import Control.Applicative import Data.Aeson-import Text.Shakespeare.Text (st)-import Language.Haskell.TH.Syntax-import Yesod.Default.Config-import Yesod.Default.Util-import Data.Text (Text)-import qualified Data.Text.Encoding as TE import Data.ByteString (ByteString)-import Data.Yaml-import Control.Applicative import Data.Default (def)+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import Data.Yaml+import Language.Haskell.TH.Syntax (Q, Exp) import Text.Hamlet---- Static setting below. Changing these requires a recompile---- | The location of static files on your system. This is a file system--- path. The default value works properly with your scaffolded site.-staticDir :: FilePath-staticDir = "static"---- | The base URL for your static files. As you can see by the default--- value, this can simply be "static" appended to your application root.--- A powerful optimization can be serving static files from a separate--- domain name. This allows you to use a web server optimized for static--- files, more easily set expires and cache values, and avoid possibly--- costly transference of cookies on static files. For more information,--- please see:---   http://code.google.com/speed/page-speed/docs/request.html#ServeFromCookielessDomain------ If you change the resource pattern for StaticR in Foundation.hs, you will--- have to make a corresponding change here.------ To see how this value is used, see urlRenderOverride in Foundation.hs-staticRoot :: AppConfig DefaultEnv x -> Text-staticRoot conf = [st|#{appRoot conf}/static|]+import Yesod.Default.Config+import Yesod.Default.Util --- | Settings for 'widgetFile', such as which template languages to support and--- default Hamlet settings. widgetFileSettings :: WidgetFileSettings widgetFileSettings = def     { wfsHamletSettings = defaultHamletSettings@@ -68,9 +39,6 @@         }     } --- The rest of this file contains settings which rarely need changing by a--- user.- widgetFile :: String -> Q Exp widgetFile = (if development then widgetFileReload                              else widgetFileNoReload)@@ -100,19 +68,26 @@     { extraHashedPwd  :: ByteString     , extraFolders :: [(Text, String)]     , extraRetag :: [RetagEntry]-    , extraFromAddress :: Text+    , extraFromAddresses :: [Text]     , extraSentBox :: Maybe FilePath     , extraMessageIDDomain :: Text+    , extraGoogleClientId :: Maybe Text+    , extraSourceLink :: Text     } deriving Show  parseExtra :: DefaultEnv -> Object -> Parser Extra-parseExtra _ o = Extra-    <$> (TE.encodeUtf8 <$> o .: "hashed-password")-    <*> (o .: "folders" >>= mapM parseFolder)-    <*> o .: "retag"-    <*> o .: "from-address"-    <*> o .:? "sent-box"-    <*> o .:? "message-id-domain" .!= "" +parseExtra _ o = do+    pwd <- encodeUtf8 <$> o .: "hashed-password"+    fdl <- o .: "folders" >>= mapM parseFolder+    retag <- o .: "retag"+    from <- o .:? "from-address" .!= "<test@example.com>"+    fromlst <- o .:? "from-addresses"+    let f = fromMaybe [from] fromlst+    sent <- o .:? "sent-box"+    dom <- o .:? "message-id-domain" .!= ""+    gcid <- o .:? "google-client-id"+    sl <- o .:? "source-link" .!= "https://bitbucket.org/wuzzeb/notmuch-web/src"+    return $ Extra pwd fdl retag f sent dom gcid sl  parseFolder :: Object -> Parser (Text, String) parseFolder o = (,) <$> o .: "name"
src/StaticFiles.hs view
@@ -19,15 +19,10 @@ import Prelude (IO) import Yesod.Static import qualified Yesod.Static as Static-import Settings (staticDir, development)+import Settings (development) --- | use this to create your static file serving site staticSite :: IO Static.Static-staticSite = if development then Static.staticDevel staticDir-                            else Static.static      staticDir+staticSite = if development then Static.staticDevel "static"+                            else Static.static      "static" --- | This generates easy references to files in the static directory at compile time,---   giving you compile-time verification that referenced files exist.---   Warning: any files added to your static directory during run-time can't be---   accessed this way. You'll have to use their FilePath or URL to access them.-$(staticFiles Settings.staticDir)+$(staticFiles "static")
− static/js/jquery-pjax-1-7-0.js
@@ -1,822 +0,0 @@-// jquery.pjax.js-// copyright chris wanstrath-// https://github.com/defunkt/jquery-pjax--(function($){--// When called on a container with a selector, fetches the href with-// ajax into the container or with the data-pjax attribute on the link-// itself.-//-// Tries to make sure the back button and ctrl+click work the way-// you'd expect.-//-// Exported as $.fn.pjax-//-// Accepts a jQuery ajax options object that may include these-// pjax specific options:-//-//-// container - Where to stick the response body. Usually a String selector.-//             $(container).html(xhr.responseBody)-//             (default: current jquery context)-//      push - Whether to pushState the URL. Defaults to true (of course).-//   replace - Want to use replaceState instead? That's cool.-//-// For convenience the second parameter can be either the container or-// the options object.-//-// Returns the jQuery object-function fnPjax(selector, container, options) {-  var context = this-  return this.on('click.pjax', selector, function(event) {-    var opts = $.extend({}, optionsFor(container, options))-    if (!opts.container)-      opts.container = $(this).attr('data-pjax') || context-    handleClick(event, opts)-  })-}--// Public: pjax on click handler-//-// Exported as $.pjax.click.-//-// event   - "click" jQuery.Event-// options - pjax options-//-// Examples-//-//   $(document).on('click', 'a', $.pjax.click)-//   // is the same as-//   $(document).pjax('a')-//-//  $(document).on('click', 'a', function(event) {-//    var container = $(this).closest('[data-pjax-container]')-//    $.pjax.click(event, container)-//  })-//-// Returns nothing.-function handleClick(event, container, options) {-  options = optionsFor(container, options)--  var link = event.currentTarget--  if (link.tagName.toUpperCase() !== 'A')-    throw "$.fn.pjax or $.pjax.click requires an anchor element"--  // Middle click, cmd click, and ctrl click should open-  // links in a new tab as normal.-  if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )-    return--  // Ignore cross origin links-  if ( location.protocol !== link.protocol || location.hostname !== link.hostname )-    return--  // Ignore anchors on the same page-  if (link.hash && link.href.replace(link.hash, '') ===-       location.href.replace(location.hash, ''))-    return--  // Ignore empty anchor "foo.html#"-  if (link.href === location.href + '#')-    return--  var defaults = {-    url: link.href,-    container: $(link).attr('data-pjax'),-    target: link,-    fragment: null-  }--  var opts = $.extend({}, defaults, options)-  var clickEvent = $.Event('pjax:click')-  $(link).trigger(clickEvent, [opts])--  if (!clickEvent.isDefaultPrevented()) {-    pjax(opts)-    event.preventDefault()-  }-}--// Public: pjax on form submit handler-//-// Exported as $.pjax.submit-//-// event   - "click" jQuery.Event-// options - pjax options-//-// Examples-//-//  $(document).on('submit', 'form', function(event) {-//    var container = $(this).closest('[data-pjax-container]')-//    $.pjax.submit(event, container)-//  })-//-// Returns nothing.-function handleSubmit(event, container, options) {-  options = optionsFor(container, options)--  var form = event.currentTarget--  if (form.tagName.toUpperCase() !== 'FORM')-    throw "$.pjax.submit requires a form element"--  var defaults = {-    type: form.method.toUpperCase(),-    url: form.action,-    data: $(form).serializeArray(),-    container: $(form).attr('data-pjax'),-    target: form,-    fragment: null-  }--  pjax($.extend({}, defaults, options))--  event.preventDefault()-}--// Loads a URL with ajax, puts the response body inside a container,-// then pushState()'s the loaded URL.-//-// Works just like $.ajax in that it accepts a jQuery ajax-// settings object (with keys like url, type, data, etc).-//-// Accepts these extra keys:-//-// container - Where to stick the response body.-//             $(container).html(xhr.responseBody)-//      push - Whether to pushState the URL. Defaults to true (of course).-//   replace - Want to use replaceState instead? That's cool.-//-// Use it just like $.ajax:-//-//   var xhr = $.pjax({ url: this.href, container: '#main' })-//   console.log( xhr.readyState )-//-// Returns whatever $.ajax returns.-function pjax(options) {-  options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)--  if ($.isFunction(options.url)) {-    options.url = options.url()-  }--  var target = options.target--  var hash = parseURL(options.url).hash--  var context = options.context = findContainerFor(options.container)--  // We want the browser to maintain two separate internal caches: one-  // for pjax'd partial page loads and one for normal page loads.-  // Without adding this secret parameter, some browsers will often-  // confuse the two.-  if (!options.data) options.data = {}-  options.data._pjax = context.selector--  function fire(type, args) {-    var event = $.Event(type, { relatedTarget: target })-    context.trigger(event, args)-    return !event.isDefaultPrevented()-  }--  var timeoutTimer--  options.beforeSend = function(xhr, settings) {-    // No timeout for non-GET requests-    // Its not safe to request the resource again with a fallback method.-    if (settings.type !== 'GET') {-      settings.timeout = 0-    }--    xhr.setRequestHeader('X-PJAX', 'true')-    xhr.setRequestHeader('X-PJAX-Container', context.selector)--    if (!fire('pjax:beforeSend', [xhr, settings]))-      return false--    if (settings.timeout > 0) {-      timeoutTimer = setTimeout(function() {-        if (fire('pjax:timeout', [xhr, options]))-          xhr.abort('timeout')-      }, settings.timeout)--      // Clear timeout setting so jquerys internal timeout isn't invoked-      settings.timeout = 0-    }--    options.requestUrl = parseURL(settings.url).href-  }--  options.complete = function(xhr, textStatus) {-    if (timeoutTimer)-      clearTimeout(timeoutTimer)--    fire('pjax:complete', [xhr, textStatus, options])--    fire('pjax:end', [xhr, options])-  }--  options.error = function(xhr, textStatus, errorThrown) {-    var container = extractContainer("", xhr, options)--    var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])-    if (options.type == 'GET' && textStatus !== 'abort' && allowed) {-      locationReplace(container.url)-    }-  }--  options.success = function(data, status, xhr) {-    // If $.pjax.defaults.version is a function, invoke it first.-    // Otherwise it can be a static string.-    var currentVersion = (typeof $.pjax.defaults.version === 'function') ?-      $.pjax.defaults.version() :-      $.pjax.defaults.version--    var latestVersion = xhr.getResponseHeader('X-PJAX-Version')--    var container = extractContainer(data, xhr, options)--    // If there is a layout version mismatch, hard load the new url-    if (currentVersion && latestVersion && currentVersion !== latestVersion) {-      locationReplace(container.url)-      return-    }--    // If the new response is missing a body, hard load the page-    if (!container.contents) {-      locationReplace(container.url)-      return-    }--    pjax.state = {-      id: options.id || uniqueId(),-      url: container.url,-      title: container.title,-      container: context.selector,-      fragment: options.fragment,-      timeout: options.timeout-    }--    if (options.push || options.replace) {-      window.history.replaceState(pjax.state, container.title, container.url)-    }--    if (container.title) document.title = container.title-    context.html(container.contents)-    executeScriptTags(container.scripts)--    // Scroll to top by default-    if (typeof options.scrollTo === 'number')-      $(window).scrollTop(options.scrollTo)--    // If the URL has a hash in it, make sure the browser-    // knows to navigate to the hash.-    if ( hash !== '' ) {-      // Avoid using simple hash set here. Will add another history-      // entry. Replace the url with replaceState and scroll to target-      // by hand.-      //-      //   window.location.hash = hash-      var url = parseURL(container.url)-      url.hash = hash--      pjax.state.url = url.href-      window.history.replaceState(pjax.state, container.title, url.href)--      var target = $(url.hash)-      if (target.length) $(window).scrollTop(target.offset().top)-    }--    fire('pjax:success', [data, status, xhr, options])-  }---  // Initialize pjax.state for the initial page load. Assume we're-  // using the container and options of the link we're loading for the-  // back button to the initial page. This ensures good back button-  // behavior.-  if (!pjax.state) {-    pjax.state = {-      id: uniqueId(),-      url: window.location.href,-      title: document.title,-      container: context.selector,-      fragment: options.fragment,-      timeout: options.timeout-    }-    window.history.replaceState(pjax.state, document.title)-  }--  // Cancel the current request if we're already pjaxing-  var xhr = pjax.xhr-  if ( xhr && xhr.readyState < 4) {-    xhr.onreadystatechange = $.noop-    xhr.abort()-  }--  pjax.options = options-  var xhr = pjax.xhr = $.ajax(options)--  if (xhr.readyState > 0) {-    if (options.push && !options.replace) {-      // Cache current container element before replacing it-      cachePush(pjax.state.id, context.clone().contents())--      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))-    }--    fire('pjax:start', [xhr, options])-    fire('pjax:send', [xhr, options])-  }--  return pjax.xhr-}--// Public: Reload current page with pjax.-//-// Returns whatever $.pjax returns.-function pjaxReload(container, options) {-  var defaults = {-    url: window.location.href,-    push: false,-    replace: true,-    scrollTo: false-  }--  return pjax($.extend(defaults, optionsFor(container, options)))-}--// Internal: Hard replace current state with url.-//-// Work for around WebKit-//   https://bugs.webkit.org/show_bug.cgi?id=93506-//-// Returns nothing.-function locationReplace(url) {-  window.history.replaceState(null, "", "#")-  window.location.replace(url)-}---var initialPop = true-var initialURL = window.location.href-var initialState = window.history.state--// Initialize $.pjax.state if possible-// Happens when reloading a page and coming forward from a different-// session history.-if (initialState && initialState.container) {-  pjax.state = initialState-}--// Non-webkit browsers don't fire an initial popstate event-if ('state' in window.history) {-  initialPop = false-}--// popstate handler takes care of the back and forward buttons-//-// You probably shouldn't use pjax on pages with other pushState-// stuff yet.-function onPjaxPopstate(event) {-  var state = event.state--  if (state && state.container) {-    // When coming forward from a seperate history session, will get an-    // initial pop with a state we are already at. Skip reloading the current-    // page.-    if (initialPop && initialURL == state.url) return--    var container = $(state.container)-    if (container.length) {-      var direction, contents = cacheMapping[state.id]--      if (pjax.state) {-        // Since state ids always increase, we can deduce the history-        // direction from the previous state.-        direction = pjax.state.id < state.id ? 'forward' : 'back'--        // Cache current container before replacement and inform the-        // cache which direction the history shifted.-        cachePop(direction, pjax.state.id, container.clone().contents())-      }--      var popstateEvent = $.Event('pjax:popstate', {-        state: state,-        direction: direction-      })-      container.trigger(popstateEvent)--      var options = {-        id: state.id,-        url: state.url,-        container: container,-        push: false,-        fragment: state.fragment,-        timeout: state.timeout,-        scrollTo: false-      }--      if (contents) {-        container.trigger('pjax:start', [null, options])--        if (state.title) document.title = state.title-        container.html(contents)-        pjax.state = state--        container.trigger('pjax:end', [null, options])-      } else {-        pjax(options)-      }--      // Force reflow/relayout before the browser tries to restore the-      // scroll position.-      container[0].offsetHeight-    } else {-      locationReplace(location.href)-    }-  }-  initialPop = false-}--// Fallback version of main pjax function for browsers that don't-// support pushState.-//-// Returns nothing since it retriggers a hard form submission.-function fallbackPjax(options) {-  var url = $.isFunction(options.url) ? options.url() : options.url,-      method = options.type ? options.type.toUpperCase() : 'GET'--  var form = $('<form>', {-    method: method === 'GET' ? 'GET' : 'POST',-    action: url,-    style: 'display:none'-  })--  if (method !== 'GET' && method !== 'POST') {-    form.append($('<input>', {-      type: 'hidden',-      name: '_method',-      value: method.toLowerCase()-    }))-  }--  var data = options.data-  if (typeof data === 'string') {-    $.each(data.split('&'), function(index, value) {-      var pair = value.split('=')-      form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))-    })-  } else if (typeof data === 'object') {-    for (key in data)-      form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))-  }--  $(document.body).append(form)-  form.submit()-}--// Internal: Generate unique id for state object.-//-// Use a timestamp instead of a counter since ids should still be-// unique across page loads.-//-// Returns Number.-function uniqueId() {-  return (new Date).getTime()-}--// Internal: Strips _pjax param from url-//-// url - String-//-// Returns String.-function stripPjaxParam(url) {-  return url-    .replace(/\?_pjax=[^&]+&?/, '?')-    .replace(/_pjax=[^&]+&?/, '')-    .replace(/[\?&]$/, '')-}--// Internal: Parse URL components and returns a Locationish object.-//-// url - String URL-//-// Returns HTMLAnchorElement that acts like Location.-function parseURL(url) {-  var a = document.createElement('a')-  a.href = url-  return a-}--// Internal: Build options Object for arguments.-//-// For convenience the first parameter can be either the container or-// the options object.-//-// Examples-//-//   optionsFor('#container')-//   // => {container: '#container'}-//-//   optionsFor('#container', {push: true})-//   // => {container: '#container', push: true}-//-//   optionsFor({container: '#container', push: true})-//   // => {container: '#container', push: true}-//-// Returns options Object.-function optionsFor(container, options) {-  // Both container and options-  if ( container && options )-    options.container = container--  // First argument is options Object-  else if ( $.isPlainObject(container) )-    options = container--  // Only container-  else-    options = {container: container}--  // Find and validate container-  if (options.container)-    options.container = findContainerFor(options.container)--  return options-}--// Internal: Find container element for a variety of inputs.-//-// Because we can't persist elements using the history API, we must be-// able to find a String selector that will consistently find the Element.-//-// container - A selector String, jQuery object, or DOM Element.-//-// Returns a jQuery object whose context is `document` and has a selector.-function findContainerFor(container) {-  container = $(container)--  if ( !container.length ) {-    throw "no pjax container for " + container.selector-  } else if ( container.selector !== '' && container.context === document ) {-    return container-  } else if ( container.attr('id') ) {-    return $('#' + container.attr('id'))-  } else {-    throw "cant get selector for pjax container!"-  }-}--// Internal: Filter and find all elements matching the selector.-//-// Where $.fn.find only matches descendants, findAll will test all the-// top level elements in the jQuery object as well.-//-// elems    - jQuery object of Elements-// selector - String selector to match-//-// Returns a jQuery object.-function findAll(elems, selector) {-  return elems.filter(selector).add(elems.find(selector));-}--function parseHTML(html) {-  return $.parseHTML(html, document, true)-}--// Internal: Extracts container and metadata from response.-//-// 1. Extracts X-PJAX-URL header if set-// 2. Extracts inline <title> tags-// 3. Builds response Element and extracts fragment if set-//-// data    - String response data-// xhr     - XHR response-// options - pjax options Object-//-// Returns an Object with url, title, and contents keys.-function extractContainer(data, xhr, options) {-  var obj = {}--  // Prefer X-PJAX-URL header if it was set, otherwise fallback to-  // using the original requested url.-  obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)--  // Attempt to parse response html into elements-  if (/<html/i.test(data)) {-    var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))-    var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))-  } else {-    var $head = $body = $(parseHTML(data))-  }--  // If response data is empty, return fast-  if ($body.length === 0)-    return obj--  // If there's a <title> tag in the header, use it as-  // the page's title.-  obj.title = findAll($head, 'title').last().text()--  if (options.fragment) {-    // If they specified a fragment, look for it in the response-    // and pull it out.-    if (options.fragment === 'body') {-      var $fragment = $body-    } else {-      var $fragment = findAll($body, options.fragment).first()-    }--    if ($fragment.length) {-      obj.contents = $fragment.contents()--      // If there's no title, look for data-title and title attributes-      // on the fragment-      if (!obj.title)-        obj.title = $fragment.attr('title') || $fragment.data('title')-    }--  } else if (!/<html/i.test(data)) {-    obj.contents = $body-  }--  // Clean up any <title> tags-  if (obj.contents) {-    // Remove any parent title elements-    obj.contents = obj.contents.not(function() { return $(this).is('title') })--    // Then scrub any titles from their descendents-    obj.contents.find('title').remove()--    // Gather all script[src] elements-    obj.scripts = findAll(obj.contents, 'script[src]').remove()-    obj.contents = obj.contents.not(obj.scripts)-  }--  // Trim any whitespace off the title-  if (obj.title) obj.title = $.trim(obj.title)--  return obj-}--// Load an execute scripts using standard script request.-//-// Avoids jQuery's traditional $.getScript which does a XHR request and-// globalEval.-//-// scripts - jQuery object of script Elements-//-// Returns nothing.-function executeScriptTags(scripts) {-  if (!scripts) return--  var existingScripts = $('script[src]')--  scripts.each(function() {-    var src = this.src-    var matchedScripts = existingScripts.filter(function() {-      return this.src === src-    })-    if (matchedScripts.length) return--    var script = document.createElement('script')-    script.type = $(this).attr('type')-    script.src = $(this).attr('src')-    document.head.appendChild(script)-  })-}--// Internal: History DOM caching class.-var cacheMapping      = {}-var cacheForwardStack = []-var cacheBackStack    = []--// Push previous state id and container contents into the history-// cache. Should be called in conjunction with `pushState` to save the-// previous container contents.-//-// id    - State ID Number-// value - DOM Element to cache-//-// Returns nothing.-function cachePush(id, value) {-  cacheMapping[id] = value-  cacheBackStack.push(id)--  // Remove all entires in forward history stack after pushing-  // a new page.-  while (cacheForwardStack.length)-    delete cacheMapping[cacheForwardStack.shift()]--  // Trim back history stack to max cache length.-  while (cacheBackStack.length > pjax.defaults.maxCacheLength)-    delete cacheMapping[cacheBackStack.shift()]-}--// Shifts cache from directional history cache. Should be-// called on `popstate` with the previous state id and container-// contents.-//-// direction - "forward" or "back" String-// id        - State ID Number-// value     - DOM Element to cache-//-// Returns nothing.-function cachePop(direction, id, value) {-  var pushStack, popStack-  cacheMapping[id] = value--  if (direction === 'forward') {-    pushStack = cacheBackStack-    popStack  = cacheForwardStack-  } else {-    pushStack = cacheForwardStack-    popStack  = cacheBackStack-  }--  pushStack.push(id)-  if (id = popStack.pop())-    delete cacheMapping[id]-}--// Public: Find version identifier for the initial page load.-//-// Returns String version or undefined.-function findVersion() {-  return $('meta').filter(function() {-    var name = $(this).attr('http-equiv')-    return name && name.toUpperCase() === 'X-PJAX-VERSION'-  }).attr('content')-}--// Install pjax functions on $.pjax to enable pushState behavior.-//-// Does nothing if already enabled.-//-// Examples-//-//     $.pjax.enable()-//-// Returns nothing.-function enable() {-  $.fn.pjax = fnPjax-  $.pjax = pjax-  $.pjax.enable = $.noop-  $.pjax.disable = disable-  $.pjax.click = handleClick-  $.pjax.submit = handleSubmit-  $.pjax.reload = pjaxReload-  $.pjax.defaults = {-    timeout: 650,-    push: true,-    replace: false,-    type: 'GET',-    dataType: 'html',-    scrollTo: 0,-    maxCacheLength: 20,-    version: findVersion-  }-  $(window).on('popstate.pjax', onPjaxPopstate)-}--// Disable pushState behavior.-//-// This is the case when a browser doesn't support pushState. It is-// sometimes useful to disable pushState for debugging on a modern-// browser.-//-// Examples-//-//     $.pjax.disable()-//-// Returns nothing.-function disable() {-  $.fn.pjax = function() { return this }-  $.pjax = fallbackPjax-  $.pjax.enable = enable-  $.pjax.disable = $.noop-  $.pjax.click = $.noop-  $.pjax.submit = $.noop-  $.pjax.reload = function() { window.location.reload() }--  $(window).off('popstate.pjax', onPjaxPopstate)-}---// Add the state property to jQuery's event object so we can use it in-// $(window).bind('popstate')-if ( $.inArray('state', $.event.props) < 0 )-  $.event.props.push('state')-- // Is pjax supported by this browser?-$.support.pjax =-  window.history && window.history.pushState && window.history.replaceState &&-  // pushState isn't reliable on iOS until 5.-  !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)--$.support.pjax ? enable() : disable()--})(jQuery);
+ static/js/jquery-pjax-1-7-3.js view
@@ -0,0 +1,838 @@+// jquery.pjax.js+// copyright chris wanstrath+// https://github.com/defunkt/jquery-pjax++(function($){++// When called on a container with a selector, fetches the href with+// ajax into the container or with the data-pjax attribute on the link+// itself.+//+// Tries to make sure the back button and ctrl+click work the way+// you'd expect.+//+// Exported as $.fn.pjax+//+// Accepts a jQuery ajax options object that may include these+// pjax specific options:+//+//+// container - Where to stick the response body. Usually a String selector.+//             $(container).html(xhr.responseBody)+//             (default: current jquery context)+//      push - Whether to pushState the URL. Defaults to true (of course).+//   replace - Want to use replaceState instead? That's cool.+//+// For convenience the second parameter can be either the container or+// the options object.+//+// Returns the jQuery object+function fnPjax(selector, container, options) {+  var context = this+  return this.on('click.pjax', selector, function(event) {+    var opts = $.extend({}, optionsFor(container, options))+    if (!opts.container)+      opts.container = $(this).attr('data-pjax') || context+    handleClick(event, opts)+  })+}++// Public: pjax on click handler+//+// Exported as $.pjax.click.+//+// event   - "click" jQuery.Event+// options - pjax options+//+// Examples+//+//   $(document).on('click', 'a', $.pjax.click)+//   // is the same as+//   $(document).pjax('a')+//+//  $(document).on('click', 'a', function(event) {+//    var container = $(this).closest('[data-pjax-container]')+//    $.pjax.click(event, container)+//  })+//+// Returns nothing.+function handleClick(event, container, options) {+  options = optionsFor(container, options)++  var link = event.currentTarget++  if (link.tagName.toUpperCase() !== 'A')+    throw "$.fn.pjax or $.pjax.click requires an anchor element"++  // Middle click, cmd click, and ctrl click should open+  // links in a new tab as normal.+  if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey )+    return++  // Ignore cross origin links+  if ( location.protocol !== link.protocol || location.hostname !== link.hostname )+    return++  // Ignore anchors on the same page+  if (link.hash && link.href.replace(link.hash, '') ===+       location.href.replace(location.hash, ''))+    return++  // Ignore empty anchor "foo.html#"+  if (link.href === location.href + '#')+    return++  var defaults = {+    url: link.href,+    container: $(link).attr('data-pjax'),+    target: link+  }++  var opts = $.extend({}, defaults, options)+  var clickEvent = $.Event('pjax:click')+  $(link).trigger(clickEvent, [opts])++  if (!clickEvent.isDefaultPrevented()) {+    pjax(opts)+    event.preventDefault()+  }+}++// Public: pjax on form submit handler+//+// Exported as $.pjax.submit+//+// event   - "click" jQuery.Event+// options - pjax options+//+// Examples+//+//  $(document).on('submit', 'form', function(event) {+//    var container = $(this).closest('[data-pjax-container]')+//    $.pjax.submit(event, container)+//  })+//+// Returns nothing.+function handleSubmit(event, container, options) {+  options = optionsFor(container, options)++  var form = event.currentTarget++  if (form.tagName.toUpperCase() !== 'FORM')+    throw "$.pjax.submit requires a form element"++  var defaults = {+    type: form.method.toUpperCase(),+    url: form.action,+    data: $(form).serializeArray(),+    container: $(form).attr('data-pjax'),+    target: form+  }++  pjax($.extend({}, defaults, options))++  event.preventDefault()+}++// Loads a URL with ajax, puts the response body inside a container,+// then pushState()'s the loaded URL.+//+// Works just like $.ajax in that it accepts a jQuery ajax+// settings object (with keys like url, type, data, etc).+//+// Accepts these extra keys:+//+// container - Where to stick the response body.+//             $(container).html(xhr.responseBody)+//      push - Whether to pushState the URL. Defaults to true (of course).+//   replace - Want to use replaceState instead? That's cool.+//+// Use it just like $.ajax:+//+//   var xhr = $.pjax({ url: this.href, container: '#main' })+//   console.log( xhr.readyState )+//+// Returns whatever $.ajax returns.+function pjax(options) {+  options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options)++  if ($.isFunction(options.url)) {+    options.url = options.url()+  }++  var target = options.target++  var hash = parseURL(options.url).hash++  var context = options.context = findContainerFor(options.container)++  // We want the browser to maintain two separate internal caches: one+  // for pjax'd partial page loads and one for normal page loads.+  // Without adding this secret parameter, some browsers will often+  // confuse the two.+  if (!options.data) options.data = {}+  options.data._pjax = context.selector++  function fire(type, args) {+    var event = $.Event(type, { relatedTarget: target })+    context.trigger(event, args)+    return !event.isDefaultPrevented()+  }++  var timeoutTimer++  options.beforeSend = function(xhr, settings) {+    // No timeout for non-GET requests+    // Its not safe to request the resource again with a fallback method.+    if (settings.type !== 'GET') {+      settings.timeout = 0+    }++    xhr.setRequestHeader('X-PJAX', 'true')+    xhr.setRequestHeader('X-PJAX-Container', context.selector)++    if (!fire('pjax:beforeSend', [xhr, settings]))+      return false++    if (settings.timeout > 0) {+      timeoutTimer = setTimeout(function() {+        if (fire('pjax:timeout', [xhr, options]))+          xhr.abort('timeout')+      }, settings.timeout)++      // Clear timeout setting so jquerys internal timeout isn't invoked+      settings.timeout = 0+    }++    options.requestUrl = parseURL(settings.url).href+  }++  options.complete = function(xhr, textStatus) {+    if (timeoutTimer)+      clearTimeout(timeoutTimer)++    fire('pjax:complete', [xhr, textStatus, options])++    fire('pjax:end', [xhr, options])+  }++  options.error = function(xhr, textStatus, errorThrown) {+    var container = extractContainer("", xhr, options)++    var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])+    if (options.type == 'GET' && textStatus !== 'abort' && allowed) {+      locationReplace(container.url)+    }+  }++  options.success = function(data, status, xhr) {+    // If $.pjax.defaults.version is a function, invoke it first.+    // Otherwise it can be a static string.+    var currentVersion = (typeof $.pjax.defaults.version === 'function') ?+      $.pjax.defaults.version() :+      $.pjax.defaults.version++    var latestVersion = xhr.getResponseHeader('X-PJAX-Version')++    var container = extractContainer(data, xhr, options)++    // If there is a layout version mismatch, hard load the new url+    if (currentVersion && latestVersion && currentVersion !== latestVersion) {+      locationReplace(container.url)+      return+    }++    // If the new response is missing a body, hard load the page+    if (!container.contents) {+      locationReplace(container.url)+      return+    }++    pjax.state = {+      id: options.id || uniqueId(),+      url: container.url,+      title: container.title,+      container: context.selector,+      fragment: options.fragment,+      timeout: options.timeout+    }++    if (options.push || options.replace) {+      window.history.replaceState(pjax.state, container.title, container.url)+    }++    // Clear out any focused controls before inserting new page contents.+    document.activeElement.blur()++    if (container.title) document.title = container.title+    context.html(container.contents)++    // FF bug: Won't autofocus fields that are inserted via JS.+    // This behavior is incorrect. So if theres no current focus, autofocus+    // the last field.+    //+    // http://www.w3.org/html/wg/drafts/html/master/forms.html+    var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]+    if (autofocusEl && document.activeElement !== autofocusEl) {+      autofocusEl.focus();+    }++    executeScriptTags(container.scripts)++    // Scroll to top by default+    if (typeof options.scrollTo === 'number')+      $(window).scrollTop(options.scrollTo)++    // If the URL has a hash in it, make sure the browser+    // knows to navigate to the hash.+    if ( hash !== '' ) {+      // Avoid using simple hash set here. Will add another history+      // entry. Replace the url with replaceState and scroll to target+      // by hand.+      //+      //   window.location.hash = hash+      var url = parseURL(container.url)+      url.hash = hash++      pjax.state.url = url.href+      window.history.replaceState(pjax.state, container.title, url.href)++      var target = $(url.hash)+      if (target.length) $(window).scrollTop(target.offset().top)+    }++    fire('pjax:success', [data, status, xhr, options])+  }+++  // Initialize pjax.state for the initial page load. Assume we're+  // using the container and options of the link we're loading for the+  // back button to the initial page. This ensures good back button+  // behavior.+  if (!pjax.state) {+    pjax.state = {+      id: uniqueId(),+      url: window.location.href,+      title: document.title,+      container: context.selector,+      fragment: options.fragment,+      timeout: options.timeout+    }+    window.history.replaceState(pjax.state, document.title)+  }++  // Cancel the current request if we're already pjaxing+  var xhr = pjax.xhr+  if ( xhr && xhr.readyState < 4) {+    xhr.onreadystatechange = $.noop+    xhr.abort()+  }++  pjax.options = options+  var xhr = pjax.xhr = $.ajax(options)++  if (xhr.readyState > 0) {+    if (options.push && !options.replace) {+      // Cache current container element before replacing it+      cachePush(pjax.state.id, context.clone().contents())++      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))+    }++    fire('pjax:start', [xhr, options])+    fire('pjax:send', [xhr, options])+  }++  return pjax.xhr+}++// Public: Reload current page with pjax.+//+// Returns whatever $.pjax returns.+function pjaxReload(container, options) {+  var defaults = {+    url: window.location.href,+    push: false,+    replace: true,+    scrollTo: false+  }++  return pjax($.extend(defaults, optionsFor(container, options)))+}++// Internal: Hard replace current state with url.+//+// Work for around WebKit+//   https://bugs.webkit.org/show_bug.cgi?id=93506+//+// Returns nothing.+function locationReplace(url) {+  window.history.replaceState(null, "", "#")+  window.location.replace(url)+}+++var initialPop = true+var initialURL = window.location.href+var initialState = window.history.state++// Initialize $.pjax.state if possible+// Happens when reloading a page and coming forward from a different+// session history.+if (initialState && initialState.container) {+  pjax.state = initialState+}++// Non-webkit browsers don't fire an initial popstate event+if ('state' in window.history) {+  initialPop = false+}++// popstate handler takes care of the back and forward buttons+//+// You probably shouldn't use pjax on pages with other pushState+// stuff yet.+function onPjaxPopstate(event) {+  var state = event.state++  if (state && state.container) {+    // When coming forward from a separate history session, will get an+    // initial pop with a state we are already at. Skip reloading the current+    // page.+    if (initialPop && initialURL == state.url) return++    // If popping back to the same state, just skip.+    // Could be clicking back from hashchange rather than a pushState.+    if (pjax.state.id === state.id) return++    var container = $(state.container)+    if (container.length) {+      var direction, contents = cacheMapping[state.id]++      if (pjax.state) {+        // Since state ids always increase, we can deduce the history+        // direction from the previous state.+        direction = pjax.state.id < state.id ? 'forward' : 'back'++        // Cache current container before replacement and inform the+        // cache which direction the history shifted.+        cachePop(direction, pjax.state.id, container.clone().contents())+      }++      var popstateEvent = $.Event('pjax:popstate', {+        state: state,+        direction: direction+      })+      container.trigger(popstateEvent)++      var options = {+        id: state.id,+        url: state.url,+        container: container,+        push: false,+        fragment: state.fragment,+        timeout: state.timeout,+        scrollTo: false+      }++      if (contents) {+        container.trigger('pjax:start', [null, options])++        if (state.title) document.title = state.title+        container.html(contents)+        pjax.state = state++        container.trigger('pjax:end', [null, options])+      } else {+        pjax(options)+      }++      // Force reflow/relayout before the browser tries to restore the+      // scroll position.+      container[0].offsetHeight+    } else {+      locationReplace(location.href)+    }+  }+  initialPop = false+}++// Fallback version of main pjax function for browsers that don't+// support pushState.+//+// Returns nothing since it retriggers a hard form submission.+function fallbackPjax(options) {+  var url = $.isFunction(options.url) ? options.url() : options.url,+      method = options.type ? options.type.toUpperCase() : 'GET'++  var form = $('<form>', {+    method: method === 'GET' ? 'GET' : 'POST',+    action: url,+    style: 'display:none'+  })++  if (method !== 'GET' && method !== 'POST') {+    form.append($('<input>', {+      type: 'hidden',+      name: '_method',+      value: method.toLowerCase()+    }))+  }++  var data = options.data+  if (typeof data === 'string') {+    $.each(data.split('&'), function(index, value) {+      var pair = value.split('=')+      form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))+    })+  } else if (typeof data === 'object') {+    for (key in data)+      form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))+  }++  $(document.body).append(form)+  form.submit()+}++// Internal: Generate unique id for state object.+//+// Use a timestamp instead of a counter since ids should still be+// unique across page loads.+//+// Returns Number.+function uniqueId() {+  return (new Date).getTime()+}++// Internal: Strips _pjax param from url+//+// url - String+//+// Returns String.+function stripPjaxParam(url) {+  return url+    .replace(/\?_pjax=[^&]+&?/, '?')+    .replace(/_pjax=[^&]+&?/, '')+    .replace(/[\?&]$/, '')+}++// Internal: Parse URL components and returns a Locationish object.+//+// url - String URL+//+// Returns HTMLAnchorElement that acts like Location.+function parseURL(url) {+  var a = document.createElement('a')+  a.href = url+  return a+}++// Internal: Build options Object for arguments.+//+// For convenience the first parameter can be either the container or+// the options object.+//+// Examples+//+//   optionsFor('#container')+//   // => {container: '#container'}+//+//   optionsFor('#container', {push: true})+//   // => {container: '#container', push: true}+//+//   optionsFor({container: '#container', push: true})+//   // => {container: '#container', push: true}+//+// Returns options Object.+function optionsFor(container, options) {+  // Both container and options+  if ( container && options )+    options.container = container++  // First argument is options Object+  else if ( $.isPlainObject(container) )+    options = container++  // Only container+  else+    options = {container: container}++  // Find and validate container+  if (options.container)+    options.container = findContainerFor(options.container)++  return options+}++// Internal: Find container element for a variety of inputs.+//+// Because we can't persist elements using the history API, we must be+// able to find a String selector that will consistently find the Element.+//+// container - A selector String, jQuery object, or DOM Element.+//+// Returns a jQuery object whose context is `document` and has a selector.+function findContainerFor(container) {+  container = $(container)++  if ( !container.length ) {+    throw "no pjax container for " + container.selector+  } else if ( container.selector !== '' && container.context === document ) {+    return container+  } else if ( container.attr('id') ) {+    return $('#' + container.attr('id'))+  } else {+    throw "cant get selector for pjax container!"+  }+}++// Internal: Filter and find all elements matching the selector.+//+// Where $.fn.find only matches descendants, findAll will test all the+// top level elements in the jQuery object as well.+//+// elems    - jQuery object of Elements+// selector - String selector to match+//+// Returns a jQuery object.+function findAll(elems, selector) {+  return elems.filter(selector).add(elems.find(selector));+}++function parseHTML(html) {+  return $.parseHTML(html, document, true)+}++// Internal: Extracts container and metadata from response.+//+// 1. Extracts X-PJAX-URL header if set+// 2. Extracts inline <title> tags+// 3. Builds response Element and extracts fragment if set+//+// data    - String response data+// xhr     - XHR response+// options - pjax options Object+//+// Returns an Object with url, title, and contents keys.+function extractContainer(data, xhr, options) {+  var obj = {}++  // Prefer X-PJAX-URL header if it was set, otherwise fallback to+  // using the original requested url.+  obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)++  // Attempt to parse response html into elements+  if (/<html/i.test(data)) {+    var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))+    var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))+  } else {+    var $head = $body = $(parseHTML(data))+  }++  // If response data is empty, return fast+  if ($body.length === 0)+    return obj++  // If there's a <title> tag in the header, use it as+  // the page's title.+  obj.title = findAll($head, 'title').last().text()++  if (options.fragment) {+    // If they specified a fragment, look for it in the response+    // and pull it out.+    if (options.fragment === 'body') {+      var $fragment = $body+    } else {+      var $fragment = findAll($body, options.fragment).first()+    }++    if ($fragment.length) {+      obj.contents = $fragment.contents()++      // If there's no title, look for data-title and title attributes+      // on the fragment+      if (!obj.title)+        obj.title = $fragment.attr('title') || $fragment.data('title')+    }++  } else if (!/<html/i.test(data)) {+    obj.contents = $body+  }++  // Clean up any <title> tags+  if (obj.contents) {+    // Remove any parent title elements+    obj.contents = obj.contents.not(function() { return $(this).is('title') })++    // Then scrub any titles from their descendants+    obj.contents.find('title').remove()++    // Gather all script[src] elements+    obj.scripts = findAll(obj.contents, 'script[src]').remove()+    obj.contents = obj.contents.not(obj.scripts)+  }++  // Trim any whitespace off the title+  if (obj.title) obj.title = $.trim(obj.title)++  return obj+}++// Load an execute scripts using standard script request.+//+// Avoids jQuery's traditional $.getScript which does a XHR request and+// globalEval.+//+// scripts - jQuery object of script Elements+//+// Returns nothing.+function executeScriptTags(scripts) {+  if (!scripts) return++  var existingScripts = $('script[src]')++  scripts.each(function() {+    var src = this.src+    var matchedScripts = existingScripts.filter(function() {+      return this.src === src+    })+    if (matchedScripts.length) return++    var script = document.createElement('script')+    script.type = $(this).attr('type')+    script.src = $(this).attr('src')+    document.head.appendChild(script)+  })+}++// Internal: History DOM caching class.+var cacheMapping      = {}+var cacheForwardStack = []+var cacheBackStack    = []++// Push previous state id and container contents into the history+// cache. Should be called in conjunction with `pushState` to save the+// previous container contents.+//+// id    - State ID Number+// value - DOM Element to cache+//+// Returns nothing.+function cachePush(id, value) {+  cacheMapping[id] = value+  cacheBackStack.push(id)++  // Remove all entires in forward history stack after pushing+  // a new page.+  while (cacheForwardStack.length)+    delete cacheMapping[cacheForwardStack.shift()]++  // Trim back history stack to max cache length.+  while (cacheBackStack.length > pjax.defaults.maxCacheLength)+    delete cacheMapping[cacheBackStack.shift()]+}++// Shifts cache from directional history cache. Should be+// called on `popstate` with the previous state id and container+// contents.+//+// direction - "forward" or "back" String+// id        - State ID Number+// value     - DOM Element to cache+//+// Returns nothing.+function cachePop(direction, id, value) {+  var pushStack, popStack+  cacheMapping[id] = value++  if (direction === 'forward') {+    pushStack = cacheBackStack+    popStack  = cacheForwardStack+  } else {+    pushStack = cacheForwardStack+    popStack  = cacheBackStack+  }++  pushStack.push(id)+  if (id = popStack.pop())+    delete cacheMapping[id]+}++// Public: Find version identifier for the initial page load.+//+// Returns String version or undefined.+function findVersion() {+  return $('meta').filter(function() {+    var name = $(this).attr('http-equiv')+    return name && name.toUpperCase() === 'X-PJAX-VERSION'+  }).attr('content')+}++// Install pjax functions on $.pjax to enable pushState behavior.+//+// Does nothing if already enabled.+//+// Examples+//+//     $.pjax.enable()+//+// Returns nothing.+function enable() {+  $.fn.pjax = fnPjax+  $.pjax = pjax+  $.pjax.enable = $.noop+  $.pjax.disable = disable+  $.pjax.click = handleClick+  $.pjax.submit = handleSubmit+  $.pjax.reload = pjaxReload+  $.pjax.defaults = {+    timeout: 650,+    push: true,+    replace: false,+    type: 'GET',+    dataType: 'html',+    scrollTo: 0,+    maxCacheLength: 20,+    version: findVersion+  }+  $(window).on('popstate.pjax', onPjaxPopstate)+}++// Disable pushState behavior.+//+// This is the case when a browser doesn't support pushState. It is+// sometimes useful to disable pushState for debugging on a modern+// browser.+//+// Examples+//+//     $.pjax.disable()+//+// Returns nothing.+function disable() {+  $.fn.pjax = function() { return this }+  $.pjax = fallbackPjax+  $.pjax.enable = enable+  $.pjax.disable = $.noop+  $.pjax.click = $.noop+  $.pjax.submit = $.noop+  $.pjax.reload = function() { window.location.reload() }++  $(window).off('popstate.pjax', onPjaxPopstate)+}+++// Add the state property to jQuery's event object so we can use it in+// $(window).bind('popstate')+if ( $.inArray('state', $.event.props) < 0 )+  $.event.props.push('state')++// Is pjax supported by this browser?+$.support.pjax =+  window.history && window.history.pushState && window.history.replaceState &&+  // pushState isn't reliable on iOS until 5.+  !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)++$.support.pjax ? enable() : disable()++})(jQuery);
+ static/js/select2-3-4-1.min.js view
@@ -0,0 +1,22 @@+/*+Copyright 2012 Igor Vaynberg++Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013++This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU+General Public License version 2 (the "GPL License"). You may choose either license to govern your+use of this software only upon the condition that you accept all of the terms of either the Apache+License or the GPL License.++You may obtain a copy of the Apache License and the GPL License at:++http://www.apache.org/licenses/LICENSE-2.0+http://www.gnu.org/licenses/gpl-2.0.html++Unless required by applicable law or agreed to in writing, software distributed under the Apache License+or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,+either express or implied. See the Apache License and the GPL License for the specific language governing+permissions and limitations under the Apache License and the GPL License.+*/+(function(a){a.fn.each2===void 0&&a.fn.extend({each2:function(b){for(var c=a([0]),d=-1,e=this.length;e>++d&&(c.context=c[0]=this[d])&&b.call(c[0],d,c)!==!1;);return this}})})(jQuery),function(a,b){"use strict";function m(a,b){for(var c=0,d=b.length;d>c;c+=1)if(o(a,b[c]))return c;return-1}function n(){var b=a(l);b.appendTo("body");var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function o(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function p(b,c){var d,e,f;if(null===b||1>b.length)return[];for(d=b.split(c),e=0,f=d.length;f>e;e+=1)d[e]=a.trim(d[e]);return d}function q(a){return a.outerWidth(!1)-a.width()}function r(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function s(c){c.on("mousemove",function(c){var d=i;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function t(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function u(a){var c,b=!1;return function(){return b===!1&&(c=a(),b=!0),c}}function v(a,b){var c=t(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){m(a.target,b.get())>=0&&c(a)})}function w(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus(),a.is(":visible")&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function x(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function y(a){a.preventDefault(),a.stopPropagation()}function z(a){a.preventDefault(),a.stopImmediatePropagation()}function A(b){if(!h){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);h=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),h.attr("class","select2-sizer"),a("body").append(h)}return h.text(b.val()),h.width()}function B(b,c,d){var e,g,f=[];e=b.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=c.attr("class"),e&&(e=""+e,a(e.split(" ")).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(this))})),b.attr("class",f.join(" "))}function C(a,c,d,e){var f=a.toUpperCase().indexOf(c.toUpperCase()),g=c.length;return 0>f?(d.push(e(a)),b):(d.push(e(a.substring(0,f))),d.push("<span class='select2-match'>"),d.push(e(a.substring(f,f+g))),d.push("</span>"),d.push(e(a.substring(f+g,a.length))),b)}function D(a){var b={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};return(a+"").replace(/[&<>"'\/\\]/g,function(a){return b[a]})}function E(c){var d,e=0,f=null,g=c.quietMillis||100,h=c.url,i=this;return function(j){window.clearTimeout(d),d=window.setTimeout(function(){e+=1;var d=e,g=c.data,k=h,l=c.transport||a.fn.select2.ajaxDefaults.transport,m={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},n=a.extend({},a.fn.select2.ajaxDefaults.params,m);g=g?g.call(i,j.term,j.page,j.context):null,k="function"==typeof k?k.call(i,j.term,j.page,j.context):k,f&&f.abort(),c.params&&(a.isFunction(c.params)?a.extend(n,c.params.call(i)):a.extend(n,c.params)),a.extend(n,{url:k,dataType:c.dataType,data:g,success:function(a){if(!(e>d)){var b=c.results(a,j.page);j.callback(b)}}}),f=l.call(i,n)},g)}}function F(c){var e,f,d=c,g=function(a){return""+a.text};a.isArray(d)&&(f=d,d={results:f}),a.isFunction(d)===!1&&(f=d,d=function(){return f});var h=d();return h.text&&(g=h.text,a.isFunction(g)||(e=h.text,g=function(a){return a[e]})),function(c){var h,e=c.term,f={results:[]};return""===e?(c.callback(d()),b):(h=function(b,d){var f,i;if(b=b[0],b.children){f={};for(i in b)b.hasOwnProperty(i)&&(f[i]=b[i]);f.children=[],a(b.children).each2(function(a,b){h(b,f.children)}),(f.children.length||c.matcher(e,g(f),b))&&d.push(f)}else c.matcher(e,g(b),b)&&d.push(b)},a(d().results).each2(function(a,b){h(b,f.results)}),c.callback(f),b)}}function G(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]};a(d?c():c).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g)}}function H(b,c){if(a.isFunction(b))return!0;if(!b)return!1;throw Error(c+" must be a function or a falsy value")}function I(b){return a.isFunction(b)?b():b}function J(b){var c=0;return a.each(b,function(a,b){b.children?c+=J(b.children):c++}),c}function K(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||1>e.tokenSeparators.length)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice.call(this,h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(o(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:b}function L(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,h,j,k,i={x:0,y:0},c={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case c.LEFT:case c.RIGHT:case c.UP:case c.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case c.SHIFT:case c.CTRL:case c.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="<div class='select2-measure-scrollbar'></div>";j=a(document),g=function(){var a=1;return function(){return a++}}(),j.on("mousemove",function(a){i.x=a.pageX,i.y=a.pageY}),d=L(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,h,i,f=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&c.element.data("select2").destroy(),this.container=this.createContainer(),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+g()),this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.body=u(function(){return c.element.closest("body")}),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.css(I(c.containerCss)),this.container.addClass(I(c.containerCssClass)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),this.dropdown.addClass(I(c.dropdownCssClass)),this.dropdown.data("select2",this),this.results=d=this.container.find(f),this.search=e=this.container.find("input.select2-input"),this.resultsPage=0,this.context=null,this.initContainer(),s(this.results),this.dropdown.on("mousemove-filtered touchstart touchmove touchend",f,this.bind(this.highlightUnderEvent)),v(80,this.results),this.dropdown.on("scroll-debounced",f,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),y(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),y(a))}),r(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",f,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown",function(a){a.stopPropagation()}),a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),k=k||n(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus()},destroy:function(){var a=this.opts.element,c=a.data("select2");this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),c!==b&&(c.container.remove(),c.dropdown.remove(),a.removeClass("select2-offscreen").removeData("select2").off(".select2").prop("autofocus",this.autofocus||!1),this.elementTabIndex?a.attr({tabindex:this.elementTabIndex}):a.removeAttr("tabindex"),a.show())},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:o(a.attr("locked"),"locked")||o(a.data("locked"),!0)}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:b},prepareOpts:function(c){var d,e,f,g,h=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw Error("Option '"+this+"' is not allowed for Select2 when attached to a <select> element.")}),c=a.extend({},{populateResults:function(d,e,f){var g,l=this.opts.id;g=function(d,e,i){var j,k,m,n,o,p,q,r,s,t;for(d=c.sortResults(d,e,f),j=0,k=d.length;k>j;j+=1)m=d[j],o=m.disabled===!0,n=!o&&l(m)!==b,p=m.children&&m.children.length>0,q=a("<li></li>"),q.addClass("select2-results-dept-"+i),q.addClass("select2-result"),q.addClass(n?"select2-result-selectable":"select2-result-unselectable"),o&&q.addClass("select2-disabled"),p&&q.addClass("select2-result-with-children"),q.addClass(h.opts.formatResultCssClass(m)),r=a(document.createElement("div")),r.addClass("select2-result-label"),t=c.formatResult(m,r,f,h.opts.escapeMarkup),t!==b&&r.html(t),q.append(r),p&&(s=a("<ul></ul>"),s.addClass("select2-result-sub"),g(m.children,s,i+1),q.append(s)),q.data("select2-data",m),e.append(q)},g(e,d,0)}},a.fn.select2.defaults,c),"function"!=typeof c.id&&(f=c.id,c.id=function(a){return a[f]}),a.isArray(c.element.data("select2Tags"))){if("tags"in c)throw"tags specified as both an attribute 'data-select2-tags' and in options of Select2 "+c.element.attr("id");c.tags=c.element.data("select2Tags")}if(e?(c.query=this.bind(function(a){var f,g,i,c={results:[],more:!1},e=a.term;i=function(b,c){var d;b.is("option")?a.matcher(e,b.text(),b)&&c.push(h.optionToData(b)):b.is("optgroup")&&(d=h.optionToData(b),b.children().each2(function(a,b){i(b,d.children)}),d.children.length>0&&c.push(d))},f=d.children(),this.getPlaceholder()!==b&&f.length>0&&(g=this.getPlaceholderOption(),g&&(f=f.not(g))),f.each2(function(a,b){i(b,c.results)}),a.callback(c)}),c.id=function(a){return a.id},c.formatResultCssClass=function(a){return a.css}):"query"in c||("ajax"in c?(g=c.element.data("ajax-url"),g&&g.length>0&&(c.ajax.url=g),c.query=E.call(c.element,c.ajax)):"data"in c?c.query=F(c.data):"tags"in c&&(c.query=G(c.tags),c.createSearchChoice===b&&(c.createSearchChoice=function(a){return{id:a,text:a}}),c.initSelection===b&&(c.initSelection=function(d,e){var f=[];a(p(d.val(),c.separator)).each(function(){var d=this,e=this,g=c.tags;a.isFunction(g)&&(g=g()),a(g).each(function(){return o(this.id,d)?(e=this.text,!1):b}),f.push({id:d,text:e})}),e(f)}))),"function"!=typeof c.query)throw"query function not defined for Select2 "+c.element.attr("id");return c},monitorSource:function(){var c,a=this.opts.element;a.on("change.select2",this.bind(function(){this.opts.element.data("select2-change-triggered")!==!0&&this.initSelection()})),c=this.bind(function(){var d,f=a.prop("disabled");f===b&&(f=!1),this.enable(!f);var d=a.prop("readonly");d===b&&(d=!1),this.readonly(d),B(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.addClass(I(this.opts.containerCssClass)),B(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(I(this.opts.dropdownCssClass))}),a.on("propertychange.select2 DOMAttrModified.select2",c),this.mutationCallback===b&&(this.mutationCallback=function(a){a.forEach(c)}),"undefined"!=typeof WebKitMutationObserver&&(this.propertyObserver&&(delete this.propertyObserver,this.propertyObserver=null),this.propertyObserver=new WebKitMutationObserver(this.mutationCallback),this.propertyObserver.observe(a.get(0),{attributes:!0,subtree:!1}))},triggerSelect:function(b){var c=a.Event("select2-selecting",{val:this.id(b),object:b});return this.opts.element.trigger(c),!c.isDefaultPrevented()},triggerChange:function(b){b=b||{},b=a.extend({},b,{type:"change",val:this.val()}),this.opts.element.data("select2-change-triggered",!0),this.opts.element.trigger(b),this.opts.element.data("select2-change-triggered",!1),this.opts.element.click(),this.opts.blurOnChange&&this.opts.element.blur()},isInterfaceEnabled:function(){return this.enabledInterface===!0},enableInterface:function(){var a=this._enabled&&!this._readonly,b=!a;return a===this.enabledInterface?!1:(this.container.toggleClass("select2-container-disabled",b),this.close(),this.enabledInterface=a,!0)},enable:function(a){return a===b&&(a=!0),this._enabled===a?!1:(this._enabled=a,this.opts.element.prop("disabled",!a),this.enableInterface(),!0)},readonly:function(a){return a===b&&(a=!1),this._readonly===a?!1:(this._readonly=a,this.opts.element.prop("readonly",a),this.enableInterface(),!0)},opened:function(){return this.container.hasClass("select2-dropdown-open")},positionDropdown:function(){var q,r,s,t,b=this.dropdown,c=this.container.offset(),d=this.container.outerHeight(!1),e=this.container.outerWidth(!1),f=b.outerHeight(!1),g=a(window).scrollLeft()+a(window).width(),h=a(window).scrollTop()+a(window).height(),i=c.top+d,j=c.left,l=h>=i+f,m=c.top-f>=this.body().scrollTop(),n=b.outerWidth(!1),o=g>=j+n,p=b.hasClass("select2-drop-above");this.opts.dropdownAutoWidth?(t=a(".select2-results",b)[0],b.addClass("select2-drop-auto-width"),b.css("width",""),n=b.outerWidth(!1)+(t.scrollHeight===t.clientHeight?0:k.width),n>e?e=n:n=e,o=g>=j+n):this.container.removeClass("select2-drop-auto-width"),"static"!==this.body().css("position")&&(q=this.body().offset(),i-=q.top,j-=q.left),p?(r=!0,!m&&l&&(r=!1)):(r=!1,!l&&m&&(r=!0)),o||(j=c.left+e-n),r?(i=c.top-f,this.container.addClass("select2-drop-above"),b.addClass("select2-drop-above")):(this.container.removeClass("select2-drop-above"),b.removeClass("select2-drop-above")),s=a.extend({top:i,left:j,width:e},I(this.opts.dropdownCss)),b.css(s)},shouldOpen:function(){var b;return this.opened()?!1:this._enabled===!1||this._readonly===!0?!1:(b=a.Event("select2-opening"),this.opts.element.trigger(b),!b.isDefaultPrevented())},clearDropdownAlignmentPreference:function(){this.container.removeClass("select2-drop-above"),this.dropdown.removeClass("select2-drop-above")},open:function(){return this.shouldOpen()?(this.opening(),!0):!1},opening:function(){function i(){return{width:Math.max(document.documentElement.scrollWidth,a(window).width()),height:Math.max(document.documentElement.scrollHeight,a(window).height())}}var f,g,b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.addClass("select2-dropdown-open").addClass("select2-container-active"),this.clearDropdownAlignmentPreference(),this.dropdown[0]!==this.body().children().last()[0]&&this.dropdown.detach().appendTo(this.body()),f=a("#select2-drop-mask"),0==f.length&&(f=a(document.createElement("div")),f.attr("id","select2-drop-mask").attr("class","select2-drop-mask"),f.hide(),f.appendTo(this.body()),f.on("mousedown touchstart click",function(b){var d,c=a("#select2-drop");c.length>0&&(d=c.data("select2"),d.opts.selectOnBlur&&d.selectHighlighted({noFocus:!0}),d.close(),b.preventDefault(),b.stopPropagation())})),this.dropdown.prev()[0]!==f[0]&&this.dropdown.before(f),a("#select2-drop").removeAttr("id"),this.dropdown.attr("id","select2-drop"),g=i(),f.css(g).show(),this.dropdown.show(),this.positionDropdown(),this.dropdown.addClass("select2-drop-active");var h=this;this.container.parents().add(window).each(function(){a(this).on(d+" "+c+" "+e,function(){var c=i();a("#select2-drop-mask").css(c),h.positionDropdown()})})},close:function(){if(this.opened()){var b=this.containerId,c="scroll."+b,d="resize."+b,e="orientationchange."+b;this.container.parents().add(window).each(function(){a(this).off(c).off(d).off(e)}),this.clearDropdownAlignmentPreference(),a("#select2-drop-mask").hide(),this.dropdown.removeAttr("id"),this.dropdown.hide(),this.container.removeClass("select2-dropdown-open"),this.results.empty(),this.clearSearch(),this.search.removeClass("select2-active"),this.opts.element.trigger(a.Event("select2-close"))}},externalSearch:function(a){this.open(),this.search.val(a),this.updateResults(!1)},clearSearch:function(){},getMaximumSelectionSize:function(){return I(this.opts.maximumSelectionSize)},ensureHighlightVisible:function(){var d,e,f,g,h,i,j,c=this.results;if(e=this.highlight(),!(0>e)){if(0==e)return c.scrollTop(0),b;d=this.findHighlightableChoices().find(".select2-result-label"),f=a(d[e]),g=f.offset().top+f.outerHeight(!0),e===d.length-1&&(j=c.find("li.select2-more-results"),j.length>0&&(g=j.offset().top+j.outerHeight(!0))),h=c.offset().top+c.outerHeight(!0),g>h&&c.scrollTop(c.scrollTop()+(g-h)),i=f.offset().top-c.offset().top,0>i&&"none"!=f.css("display")&&c.scrollTop(c.scrollTop()+i)}},findHighlightableChoices:function(){return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)")},moveHighlight:function(b){for(var c=this.findHighlightableChoices(),d=this.highlight();d>-1&&c.length>d;){d+=b;var e=a(c[d]);if(e.hasClass("select2-result-selectable")&&!e.hasClass("select2-disabled")&&!e.hasClass("select2-selected")){this.highlight(d);break}}},highlight:function(c){var e,f,d=this.findHighlightableChoices();return 0===arguments.length?m(d.filter(".select2-highlighted")[0],d.get()):(c>=d.length&&(c=d.length-1),0>c&&(c=0),this.results.find(".select2-highlighted").removeClass("select2-highlighted"),e=a(d[c]),e.addClass("select2-highlighted"),this.ensureHighlightVisible(),f=e.data("select2-data"),f&&this.opts.element.trigger({type:"select2-highlight",val:this.id(f),choice:f}),b)},countSelectableResults:function(){return this.findHighlightableChoices().length},highlightUnderEvent:function(b){var c=a(b.target).closest(".select2-result-selectable");if(c.length>0&&!c.is(".select2-highlighted")){var d=this.findHighlightableChoices();this.highlight(d.index(c))}else 0==c.length&&this.results.find(".select2-highlighted").removeClass("select2-highlighted")},loadMoreIfNeeded:function(){var c,a=this.results,b=a.find("li.select2-more-results"),e=this.resultsPage+1,f=this,g=this.search.val(),h=this.context;0!==b.length&&(c=b.offset().top-a.offset().top-a.height(),this.opts.loadMorePadding>=c&&(b.addClass("select2-active"),this.opts.query({element:this.opts.element,term:g,page:e,context:h,matcher:this.opts.matcher,callback:this.bind(function(c){f.opened()&&(f.opts.populateResults.call(this,a,c.results,{term:g,page:e,context:h}),f.postprocessResults(c,!1,!1),c.more===!0?(b.detach().appendTo(a).text(f.opts.formatLoadMore(e+1)),window.setTimeout(function(){f.loadMoreIfNeeded()},10)):b.remove(),f.positionDropdown(),f.resultsPage=e,f.context=c.context)})})))},tokenize:function(){},updateResults:function(c){function l(){d.removeClass("select2-active"),h.positionDropdown()}function m(a){e.html(a),l()}var g,i,d=this.search,e=this.results,f=this.opts,h=this,j=d.val(),k=a.data(this.container,"select2-last-term");if((c===!0||!k||!o(j,k))&&(a.data(this.container,"select2-last-term",j),c===!0||this.showSearchInput!==!1&&this.opened())){var n=this.getMaximumSelectionSize();if(n>=1&&(g=this.data(),a.isArray(g)&&g.length>=n&&H(f.formatSelectionTooBig,"formatSelectionTooBig")))return m("<li class='select2-selection-limit'>"+f.formatSelectionTooBig(n)+"</li>"),b;if(d.val().length<f.minimumInputLength)return H(f.formatInputTooShort,"formatInputTooShort")?m("<li class='select2-no-results'>"+f.formatInputTooShort(d.val(),f.minimumInputLength)+"</li>"):m(""),c&&this.showSearch&&this.showSearch(!0),b;if(f.maximumInputLength&&d.val().length>f.maximumInputLength)return H(f.formatInputTooLong,"formatInputTooLong")?m("<li class='select2-no-results'>"+f.formatInputTooLong(d.val(),f.maximumInputLength)+"</li>"):m(""),b;f.formatSearching&&0===this.findHighlightableChoices().length&&m("<li class='select2-searching'>"+f.formatSearching()+"</li>"),d.addClass("select2-active"),i=this.tokenize(),i!=b&&null!=i&&d.val(i),this.resultsPage=1,f.query({element:f.element,term:d.val(),page:this.resultsPage,context:null,matcher:f.matcher,callback:this.bind(function(g){var i;return this.opened()?(this.context=g.context===b?null:g.context,this.opts.createSearchChoice&&""!==d.val()&&(i=this.opts.createSearchChoice.call(h,d.val(),g.results),i!==b&&null!==i&&h.id(i)!==b&&null!==h.id(i)&&0===a(g.results).filter(function(){return o(h.id(this),h.id(i))}).length&&g.results.unshift(i)),0===g.results.length&&H(f.formatNoMatches,"formatNoMatches")?(m("<li class='select2-no-results'>"+f.formatNoMatches(d.val())+"</li>"),b):(e.empty(),h.opts.populateResults.call(this,e,g.results,{term:d.val(),page:this.resultsPage,context:null}),g.more===!0&&H(f.formatLoadMore,"formatLoadMore")&&(e.append("<li class='select2-more-results'>"+h.opts.escapeMarkup(f.formatLoadMore(this.resultsPage))+"</li>"),window.setTimeout(function(){h.loadMoreIfNeeded()},10)),this.postprocessResults(g,c),l(),this.opts.element.trigger({type:"select2-loaded",items:g}),b)):(this.search.removeClass("select2-active"),b)})})}},cancel:function(){this.close()},blur:function(){this.opts.selectOnBlur&&this.selectHighlighted({noFocus:!0}),this.close(),this.container.removeClass("select2-container-active"),this.search[0]===document.activeElement&&this.search.blur(),this.clearSearch(),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus")},focusSearch:function(){w(this.search)},selectHighlighted:function(a){var b=this.highlight(),c=this.results.find(".select2-highlighted"),d=c.closest(".select2-result").data("select2-data");d?(this.highlight(b),this.onSelect(d,a)):a&&a.noFocus&&this.close()},getPlaceholder:function(){var a;return this.opts.element.attr("placeholder")||this.opts.element.attr("data-placeholder")||this.opts.element.data("placeholder")||this.opts.placeholder||((a=this.getPlaceholderOption())!==b?a.text():b)},getPlaceholderOption:function(){if(this.select){var a=this.select.children().first();if(this.opts.placeholderOption!==b)return"first"===this.opts.placeholderOption&&a||"function"==typeof this.opts.placeholderOption&&this.opts.placeholderOption(this.select);if(""===a.text()&&""===a.val())return a}},initContainerWidth:function(){function c(){var c,d,e,f,g;if("off"===this.opts.width)return null;if("element"===this.opts.width)return 0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px";if("copy"===this.opts.width||"resolve"===this.opts.width){if(c=this.opts.element.attr("style"),c!==b)for(d=c.split(";"),f=0,g=d.length;g>f;f+=1)if(e=d[f].replace(/\s/g,"").match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i),null!==e&&e.length>=1)return e[1];return"resolve"===this.opts.width?(c=this.opts.element.css("width"),c.indexOf("%")>0?c:0===this.opts.element.outerWidth(!1)?"auto":this.opts.element.outerWidth(!1)+"px"):null}return a.isFunction(this.opts.width)?this.opts.width():this.opts.width}var d=c.call(this);null!==d&&this.container.css("width",d)}}),e=L(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container"}).html(["<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>","   <span class='select2-chosen'>&nbsp;</span><abbr class='select2-search-choice-close'></abbr>","   <span class='select2-arrow'><b></b></span>","</a>","<input class='select2-focusser select2-offscreen' type='text'/>","<div class='select2-drop select2-display-none'>","   <div class='select2-search'>","       <input type='text' autocomplete='off' autocorrect='off' autocapitalize='off' spellcheck='false' class='select2-input'/>","   </div>","   <ul class='select2-results'>","   </ul>","</div>"].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var b,c,d;this.opts.minimumResultsForSearch>=0&&this.showSearch(!0),this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.search.focus(),b=this.search.get(0),b.createTextRange?(c=b.createTextRange(),c.collapse(!1),c.select()):b.setSelectionRange&&(d=this.search.val().length,b.setSelectionRange(d,d)),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.removeAttr("disabled"),this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.removeAttr("disabled"),this.focusser.focus()},initContainer:function(){var d,e=this.container,f=this.dropdown;0>this.opts.minimumResultsForSearch?this.showSearch(!1):this.showSearch(!0),this.selection=d=e.find(".select2-choice"),this.focusser=e.find(".select2-focusser"),this.focusser.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.focusser.attr("id")),this.focusser.attr("tabindex",this.elementTabIndex),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){if(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)return y(a),b;switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),b;case c.ESC:return this.cancel(a),y(a),b}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body().get(0)&&window.setTimeout(this.bind(function(){this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&&a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.ESC){if(this.opts.openOnEnter===!1&&a.which===c.ENTER)return y(a),b;if(a.which==c.DOWN||a.which==c.UP||a.which==c.ENTER&&this.opts.openOnEnter){if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return;return this.open(),y(a),b}return a.which==c.DELETE||a.which==c.BACKSPACE?(this.opts.allowClear&&this.clear(),y(a),b):b}})),r(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){if(this.opts.minimumResultsForSearch>=0){if(a.stopPropagation(),this.opened())return;this.open()}})),d.on("mousedown","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),z(a),this.close(),this.selection.focus())})),d.on("mousedown",this.bind(function(b){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),y(b)})),f.on("mousedown",this.bind(function(){this.search.focus()})),d.on("focus",this.bind(function(a){y(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.setPlaceholder()},clear:function(a){var b=this.selection.data("select2-data");if(b){var c=this.getPlaceholderOption();this.opts.element.val(c?c.val():""),this.selection.find(".select2-chosen").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),a!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(b),choice:b}),this.triggerChange({removed:b}))}},initSelection:function(){if(this.isPlaceholderOptionSelected())this.updateSelection([]),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder())})}},isPlaceholderOptionSelected:function(){var a;return(a=this.getPlaceholderOption())!==b&&a.is(":selected")||""===this.opts.element.val()||this.opts.element.val()===b||null===this.opts.element.val()},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find(":selected");b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=o(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&this.getPlaceholderOption()===b?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(this.isPlaceholderOptionSelected()&&a!==b){if(this.select&&this.getPlaceholderOption()===b)return;this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,c,d){var e=0,f=this;if(this.findHighlightableChoices().each2(function(a,c){return o(f.id(c.data("select2-data")),f.opts.element.val())?(e=a,!1):b}),d!==!1&&(c===!0&&e>=0?this.highlight(e):this.highlight(0)),c===!0){var h=this.opts.minimumResultsForSearch;h>=0&&this.showSearch(J(a.results)>=h)}},showSearch:function(b){this.showSearchInput!==b&&(this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b))},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.close(),b&&b.noFocus||this.selection.focus(),o(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,e,c=this.selection.find(".select2-chosen");this.selection.data("select2-data",a),c.empty(),d=this.opts.formatSelection(a,c,this.opts.escapeMarkup),d!==b&&c.append(d),e=this.opts.formatSelectionCssClass(a,c),e!==b&&c.addClass(e),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear")+},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find(":selected").each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(!a&&0!==a)return this.clear(c),b;if(this.opts.initSelection===b)throw Error("cannot call val() if initSelection() is not defined");this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a,c){var d;return 0===arguments.length?(d=this.selection.data("select2-data"),d==b&&(d=null),d):(a&&""!==a?(d=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),c&&this.triggerChange({added:a,removed:d})):this.clear(c),b)}}),f=L(d,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html(["<ul class='select2-choices'>","  <li class='select2-search-field'>","    <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'>","  </li>","</ul>","<div class='select2-drop select2-drop-multi select2-display-none'>","   <ul class='select2-results'>","   </ul>","</div>"].join(""));return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find(":selected").each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=p(c.val(),b.separator),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return o(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;e.length>c;c++)for(var g=e[c],h=0;f.length>h;h++){var i=f[h];if(o(g,b.id(i))){a.push(i),f.splice(h,1);break}}d(a)}:a.noop})}),b},selectChoice:function(a){var b=this.container.find(".select2-search-choice-focus");b.length&&a&&a[0]==b[0]||(b.length&&this.opts.element.trigger("choice-deselected",b),b.removeClass("select2-search-choice-focus"),a&&a.length&&(this.close(),a.addClass("select2-search-choice-focus"),this.opts.element.trigger("choice-selected",a)))},initContainer:function(){var e,d=".select2-choices";this.searchContainer=this.container.find(".select2-search-field"),this.selection=e=this.container.find(d);var f=this;this.selection.on("mousedown",".select2-search-choice",function(){f.search[0].focus(),f.selectChoice(a(this))}),this.search.attr("id","s2id_autogen"+g()),a("label[for='"+this.opts.element.attr("id")+"']").attr("for",this.search.attr("id")),this.search.on("input paste",this.bind(function(){this.isInterfaceEnabled()&&(this.opened()||this.open())})),this.search.attr("tabindex",this.elementTabIndex),this.keydowns=0,this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()){++this.keydowns;var d=e.find(".select2-search-choice-focus"),f=d.prev(".select2-search-choice:not(.select2-locked)"),g=d.next(".select2-search-choice:not(.select2-locked)"),h=x(this.search);if(d.length&&(a.which==c.LEFT||a.which==c.RIGHT||a.which==c.BACKSPACE||a.which==c.DELETE||a.which==c.ENTER)){var i=d;return a.which==c.LEFT&&f.length?i=f:a.which==c.RIGHT?i=g.length?g:null:a.which===c.BACKSPACE?(this.unselect(d.first()),this.search.width(10),i=f.length?f:g):a.which==c.DELETE?(this.unselect(d.first()),this.search.width(10),i=g.length?g:null):a.which==c.ENTER&&(i=null),this.selectChoice(i),y(a),i&&i.length||this.open(),b}if((a.which===c.BACKSPACE&&1==this.keydowns||a.which==c.LEFT)&&0==h.offset&&!h.length)return this.selectChoice(e.find(".select2-search-choice:not(.select2-locked)").last()),y(a),b;if(this.selectChoice(null),this.opened())switch(a.which){case c.UP:case c.DOWN:return this.moveHighlight(a.which===c.UP?-1:1),y(a),b;case c.ENTER:return this.selectHighlighted(),y(a),b;case c.TAB:return this.selectHighlighted({noFocus:!0}),this.close(),b;case c.ESC:return this.cancel(a),y(a),b}if(a.which!==c.TAB&&!c.isControl(a)&&!c.isFunctionKey(a)&&a.which!==c.BACKSPACE&&a.which!==c.ESC){if(a.which===c.ENTER){if(this.opts.openOnEnter===!1)return;if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return}this.open(),(a.which===c.PAGE_UP||a.which===c.PAGE_DOWN)&&y(a),a.which===c.ENTER&&y(a)}}})),this.search.on("keyup",this.bind(function(){this.keydowns=0,this.resizeSearch()})),this.search.on("blur",this.bind(function(b){this.container.removeClass("select2-container-active"),this.search.removeClass("select2-focused"),this.selectChoice(null),this.opened()||this.clearSearch(),b.stopImmediatePropagation(),this.opts.element.trigger(a.Event("select2-blur"))})),this.container.on("click",d,this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").length>0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",d,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.addClass("select2-offscreen"),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),this.updateResults(!0),this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){0>m(e.id(this),c)&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer.call(this,a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,b){this.triggerSelect(a)&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()&&this.updateResults(!0),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),b&&b.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,k,d=!c.locked,e=a("<li class='select2-search-choice'>    <div></div>    <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a></li>"),f=a("<li class='select2-search-choice select2-locked'><div></div></li>"),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div"),this.opts.escapeMarkup),j!=b&&g.find("div").replaceWith("<div>"+j+"</div>"),k=this.opts.formatSelectionCssClass(c,g.find("div")),k!=b&&g.addClass(k),d&&g.find(".select2-search-choice-close").on("mousedown",y).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(a(b.target).closest(".select2-search-choice").fadeOut("fast",this.bind(function(){this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),this.close(),this.focusSearch()})).dequeue(),y(b))})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(a){var c,d,b=this.getVal();if(a=a.closest(".select2-search-choice"),0===a.length)throw"Invalid argument: "+a+". Must be .select2-search-choice";c=a.data("select2-data"),c&&(d=m(this.id(c),b),d>=0&&(b.splice(d,1),this.setVal(b),this.select&&this.postprocessResults()),a.remove(),this.opts.element.trigger({type:"removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));m(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&(!a||a&&!a.more&&0===this.results.find(".select2-no-results").length)&&H(g.opts.formatNoMatches,"formatNoMatches")&&this.results.append("<li class='select2-no-results'>"+g.opts.formatNoMatches(g.search.val())+"</li>")},getMaxSearchWidth:function(){return this.selection.width()-q(this.search)},resizeSearch:function(){var a,b,c,d,e,f=q(this.search);a=A(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(e)},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),p(a,this.opts.separator))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){0>m(this,c)&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;b.length>c;c++)for(var d=0;a.length>d;d++)o(this.opts.id(b[c]),this.opts.id(a[d]))&&(b.splice(c,1),c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),b;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a.map(b,f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(this.buildChangeDetails(e,this.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(c,d){var f,g,e=this;return 0===arguments.length?this.selection.find(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(g=this.data(),c||(c=[]),f=a.map(c,function(a){return e.opts.id(a)}),this.setVal(f),this.updateSelection(c),this.clearSearch(),d&&this.triggerChange(this.buildChangeDetails(g,this.data())),b)}}),a.fn.select2=function(){var d,g,h,i,j,c=Array.prototype.slice.call(arguments,0),k=["val","destroy","opened","open","close","focus","isFocused","container","dropdown","onSortStart","onSortEnd","enable","readonly","positionDropdown","data","search"],l=["val","opened","isFocused","container","data"],n={search:"externalSearch"};return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?j=d.element.prop("multiple"):(j=d.multiple||!1,"tags"in d&&(d.multiple=j=!0)),g=j?new f:new e,g.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(0>m(c[0],k))throw"Unknown method: "+c[0];if(i=b,g=a(this).data("select2"),g===b)return;if(h=c[0],"container"===h?i=g.container:"dropdown"===h?i=g.dropdown:(n[h]&&(h=n[h]),i=g[h].apply(g,c.slice(1))),m(c[0],l)>=0)return!1}}),i===b?this:i},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return C(a.text,c.term,e,d),e.join("")},formatSelection:function(a,c,d){return a?d(a.text):b},sortResults:function(a){return a},formatResultCssClass:function(){return b},formatSelectionCssClass:function(){return b},formatNoMatches:function(){return"No matches found"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results..."},formatSearching:function(){return"Searching..."},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a.id},matcher:function(a,b){return(""+b).toUpperCase().indexOf((""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:K,escapeMarkup:D,blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null}},a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:E,local:F,tags:G},util:{debounce:t,markMatch:C,escapeMarkup:D},"class":{"abstract":d,single:e,multi:f}}}}(jQuery);
+ templates/abook.julius view
@@ -0,0 +1,44 @@+$(document).ready(function() {+    var aType = $("#addressbooktype");++    var searchAbook = function(input, page, callback) {+        $.ajax({ url: "@{AbookQueryR}"+               , dataType: 'json'+               , data: { q : input }+        })+        .done(function(data) {+            addrs = [];+            if (data) {+                $(data).each(function() {+                    addrs.push({id: this, text: this});+                });+            }+            if (callback)+                callback({results: addrs, more: false});+            else+                console.log(addrs);+        })+        .fail(function(xhr, st, err) {+            console.log("Error " + err);+        });+    };++    //Set the address book type based on the dropdown.  The google+    //contacts is set in a seperate change handler which is only+    //loaded depending on the settings.+    aType.change(function() {+        var v = $(this).val();+        window.localStorage.setItem("Address Book Type", v);+        if (v == 0)+            window.searchContacts = undefined;+        else if (v == 1)+            window.searchContacts = searchAbook;+    });++    // Load the old format type+    var oldformat = window.localStorage.getItem("Address Book Type");+    if (oldformat) {+        aType.val(oldformat);+        aType.change();+    }+});
templates/compose.hamlet view
@@ -1,8 +1,18 @@ $forall e <- err     <div .alert>         #{e}-<form .form-horizontal enctype=#{enctype} method=post action=@{ComposeR}>+<form enctype=#{enctype} method=post action=@{ComposeR}>     ^{widget}     <div .control-group>         <div .controls>             <button type="submit" .btn .btn-primary>_{MsgSend}+            <button type="button" .btn .hidden #previewbtn>_{MsgPreview}+<form #preview-form enctype=#{previewEnctype} style="display:none" method=post action=@{PreviewMessageR}>+    ^{previewWidget}+<div .modal .hide .fade #preview-modal>+    <div .modal-header>+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;+        <h3>_{MsgPreview}+    <div .modal-body #preview-body>+    <div .modal-footer>+        <button type="button" .btn data-dismiss="modal" aria-hidden="true">_{MsgClose}
templates/compose.julius view
@@ -1,5 +1,5 @@ $(document).ready(function() {-    //copied from jquery-mobile+    //resize body field: copied from jquery-mobile     var input = $("textarea#body"),         extraLineHeight = 15,         keyupTimeoutBuffer = 100,@@ -14,8 +14,137 @@         }     }; +    $(".addr-help").popover();+     input.on( "keyup change input paste", function() {         clearTimeout( keyupTimeout );         keyupTimeout = setTimeout( input._keyup, keyupTimeoutBuffer );+    });++    //restore format setting+    var oldformat = window.localStorage.getItem("Compose Body Format");+    if (oldformat) {+        $("#bdyfmt").val(oldformat);+        if (oldformat == 3) { //markdown+            $("#previewbtn").removeClass("hidden");+        }+    }+    $("#bdyfmt").change(function() {+        var f = $("#bdyfmt").val();+        window.localStorage.setItem("Compose Body Format", f);+        if (f == 3) { //markdown+            $("#previewbtn").removeClass("hidden");+        } else {+            $("#previewbtn").addClass("hidden");+        }+    });++    //The preview button+    $("#previewbtn").on("click", function() {+        var frm = $("#preview-form");+        var data = frm.serializeArray();+        $(data).each(function() {+            if (this.name == "f2")+                this.value = $("#Body").val();+        });+        $.ajax({+            type: "POST",+            data: $.param(data),+            dataType: 'html',+            url: frm.attr('action')+        })+        .done(function(data) {+            $("#preview-body").html(data);+            $("#preview-modal").modal();+        })+        .fail(function(xhr, err, e) {+            alert("Error " + err + " " + e);+        });+        return false;+    });++    //parse a comma seperated list of addresses, supporting commas inside quotes in the name+    var parseAddrs = function(str) {+        var pttrn = /( *"[^"]*"[^,"]*|[^",]+)(,)?/g;+        var data = [];++        while (m = pttrn.exec(str)) {+            if (m[1]) {+                data.push({txt: m[1], comma: m[2] !== undefined});+            }+        }++        return data;+    };++    // Now the address fields+    $(".address-field").each(function() {+        $(this).select2({+            data: [],+            multiple: true,+            selectOnBlur: true,+            width: 'element',+            minimumInputLength: 3,+            createSearchChoice: function(term) {+                return {id: term, text: term};+            },+            initSelection: function(element, callback) {+                var data = []+                $(parseAddrs(element.val())).each(function() {+                    data.push({id: this.txt, text: this.txt});+                });+                callback(data);+            },+            query: function(options) {+                if (window.searchContacts)+                    window.searchContacts(options.term, options.page, options.callback);+                else+                    options.callback({results:[], more: false});+            },+            tokenizer: function(input, selection, selectCallback, opts) {+                var data = parseAddrs(input);+                var ret = '';+                $(data).each(function() {+                    if (this.comma) {+                        selectCallback({id: this.txt, text: this.txt});+                    } else {+                        ret += this.txt;+                    }+                });+                return ret;+            },+            formatSelection: function(addr, node) {+                if (addr.id != "") {+                    var esc = this.escapeMarkup;+                    var m = addr.id.match(/([^<]*)(<([^>]*)>)?/);+                    if (m === undefined || m[1] === undefined) {+                        return addr.text;+                    } else {+                        node.replaceWith("<div title='"+esc(addr.text)+"'>"+esc(m[1])+"</div>");+                        return undefined;+                    }+                }+            },+        });++        var addrbox = $(this);+        //var s2container = addrbox.prev("div.select2-container");+        var s2container = addrbox.select2("container");+        s2container.on("dblclick", "li.select2-search-choice", function() {+            var data = addrbox.select2("data");++            //Clear the clicked element+            var item = $(this).children("div").attr("title");+            var ndata = $.grep(data, function(i) {+                return i.id != item;+            });+            addrbox.select2("data", ndata);++            //Set it as the input+            s2container.find("li.select2-search-field input").val(item);+            addrbox.data("select2").resizeSearch();+            addrbox.data("select2").focusSearch();+            addrbox.select2("open");+        });     }); });
templates/compose.lucius view
@@ -1,20 +1,20 @@-textarea#body {+textarea#Body {     height: 200px; }-textarea#extraheaders {+textarea#hdrs {     white-space: nowrap;     overflow: auto; } @media (max-width: 800px) {-    textarea#body {+    textarea#Body, input#subj {         width: 100%;     } } @media (min-width: 801px) {-    textarea#body {+    textarea#Body, input#subj {         width: 650px;     }-    textarea#extraheaders, div.controls input {+    textarea#hdrs, div.controls input, div.controls select {         width: 400px;     } }
templates/default-layout-wrapper.hamlet view
@@ -9,5 +9,12 @@         <meta name="viewport" content="width=device-width,initial-scale=1">         <link rel="search" href=@{OpenSearchR} type="application/opensearchdescription+xml" title="Notmuch Web">         ^{pageHead pc}+        <noscript>+            <style>+                td.only-noscript { display: table-cell; }+                th.only-noscript { display: inline; }+                .hide-noscript { display: none; }+                .collapse { height: auto; }+                td.search-link { cursor: auto; }     <body>         ^{pageBody pc}
templates/default-layout.cassius view
@@ -21,6 +21,8 @@     body         padding-right: 5px         padding-left: 5px+.only-noscript+    display: none  /* Fix for https://github.com/twitter/bootstrap/issues/7968 */ .dropdown-backdrop
templates/default-layout.hamlet view
@@ -41,8 +41,9 @@    <footer>     <p>-      <a href="http://notmuchmail.org/">Notmuch</a> is released under the-      <a href="http://www.gnu.org/licenses/gpl.html">GPLv3+</a>.-      Notmuch-web is released under the-      <a href="http://www.gnu.org/licenses/agpl.html">AGPLv3+</a>(-      <a href="https://bitbucket.org/wuzzeb/notmuch-web">source</a>).+      <a href="http://notmuchmail.org/">Notmuch+      is released under the <a href="http://www.gnu.org/licenses/gpl.html">GPLv3+</a>.+      <a href="https://bitbucket.org/wuzzeb/notmuch-web">Notmuch-web+      is released under the+      <a href="http://www.gnu.org/licenses/agpl.html">AGPLv3++      (<a href="#{sourceLink}">source</a>).
+ templates/google-contacts.hamlet view
@@ -0,0 +1,8 @@+<div .control-group>+    <label .control-label for="addressbooktype">_{MsgAddrBookType}+    <div .controls>+        <select name="addressbooktype" #addressbooktype>+            <option value="0">_{MsgNoAddressBook}+            <option value="1">_{MsgAbook}+            <option value="2">_{MsgGoogle}+        <button #google-auth-button .btn .hidden data-notmuch-client-id="#{clientID}">_{MsgGoogleAuth}
+ templates/google-contacts.julius view
@@ -0,0 +1,80 @@+//Some documentation+//https://developers.google.com/api-client-library/javascript/features/authentication+//https://developers.google.com/api-client-library/javascript/features/cors+//https://developers.google.com/google-apps/contacts/v3/++// loadGoogleAuth will be called once the google authentication script has+// been loaded, and we don't load the script until the document is ready so+// the body of loadGoogleAuth can assume the document is ready.+function loadGoogleAuth() {+    var btn = $("#google-auth-button");+++    var handleAuth = function(authResult) {+        if (authResult && !authResult.error) {+            btn.addClass("hidden");+        } else {+            btn.removeClass("hidden");+        }+    };++    btn.on('click', function() {+        gapi.auth.authorize({client_id: btn.data("notmuch-client-id")+                            , scope: 'https://www.google.com/m8/feeds'+                            , immediate: false+                            }, handleAuth);++    });++    // Check auth on document load+    gapi.auth.authorize({client_id: btn.data("notmuch-client-id")+                        , scope: 'https://www.google.com/m8/feeds'+                        , immediate: true+                        }, handleAuth);++    var searchGoogleContacts = function(input, page, callback) {+        $.ajax({ url: 'https://www.google.com/m8/feeds/contacts/default/thin'+               , dataType: 'json'+               , data: { 'access_token' : gapi.auth.getToken().access_token+                       , q : input+                       , 'max-results' : 20+                       , 'start-index': (page-1)*20 + 1+                       , 'alt' : "json"+                       , 'v' : '3.0'+                       }+        })+        .done(function(data) {+            var num = data.feed["openSearch$totalResults"]["$t"];+            var addrs = [];+            $(data.feed.entry).each(function() {+                var name = this.title["$t"];+                if ("gd$email" in this) {+                    $(this["gd$email"]).each(function() {+                        var a = name + " <" + this.address + ">"+                        addrs.push({id: a, text: a});+                    });+                }+            });+            if (callback)+                callback({results: addrs, more: num >= 20});+            else+                console.log(addrs);+        })+        .fail(function(xhr, st, err) {+            alert("Error " + err);+        });+    };++    // Set the address book based on the dropdown+    $("#addressbooktype").change(function() {+        if ($(this).val() == 2)+            window.searchContacts = searchGoogleContacts;+    });++    if ($("#addressbooktype").val() == 2)+        window.searchContacts = searchGoogleContacts;+}++$(document).ready(function() {+    $.getScript("https://apis.google.com/js/auth.js?onload=loadGoogleAuth");+});
+ templates/plain-contacts.hamlet view
@@ -0,0 +1,6 @@+<div .control-group>+    <label .control-label for="addressbooktype">_{MsgAddrBookType}+    <div .controls>+        <select name="addressbooktype" #addressbooktype>+            <option value="0">_{MsgNoAddressBook}+            <option value="1">_{MsgAbook}
templates/search.hamlet view
@@ -1,8 +1,9 @@ <p>-    <a href=@{ThreadPagerR s}>_{MsgViewInPager}+    <a .hide-noscript href=@{ThreadPagerR s}>_{MsgViewInPager} <table .table .table-striped .table-condensed>   <thead>     <tr>+      <th .only-noscript>       <th>_{MsgDate}       <th>_{MsgSubject}       <th>_{MsgAuthors}@@ -10,6 +11,8 @@   <tbody>     $forall r <- search       <tr>+        <td .only-noscript>+            <a href="@{ThreadR (searchThread r)}">Open         <td .search-link onclick="document.location = '@{ThreadR (searchThread r)}'">#{searchDateRel r}         <td .search-link onclick="document.location = '@{ThreadR (searchThread r)}'">#{searchSubject r}         <td .search-link onclick="document.location = '@{ThreadR (searchThread r)}'">#{searchAuthors r}
tests/main.hs view
@@ -11,10 +11,13 @@ import Application (makeFoundation)  import HomeTest+import Address  main :: IO () main = do     conf <- loadConfig $ (configSettings Testing) { csParseExtra = parseExtra }     foundation <- makeFoundation conf-    hspec $ yesodSpec foundation $-        homeSpecs+    hspec $ do+        yesodSpec foundation $+          homeSpecs+        addrSpecs