diff --git a/.gitignore b/.gitignore index 7e5ff3d1..588f83e4 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,5 @@ pom.xml.asc .clj-kondo/* !.clj-kondo/config.edn *.iml +/node_modules/ +/package.json diff --git a/README.md b/README.md index 2d0b2d40..1552fc4d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -![Xiana logo](resources/images/Xiana.png) +![Xiana logo](doc/resources/images/Xiana.png) + # Xiana framework Xiana is a lightweight web-application framework written in Clojure, for Clojure. The goal is to be simple, fast, and -most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the wonders -of functional programming! +most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the +wonders of functional programming! It's easy to install, fun to experiment with, and a powerful tool to produce monolithic web applications. @@ -16,7 +17,9 @@ Xiana has its own Leiningen template, so you can create a skeleton project with ```shell lein new xiana app ``` -[getting-started](./doc/getting-started.md) explains how to use this to create a very simple app with a db, a backend offering an API, and a frontend that displays something from the database. + +[getting-started](./doc/getting-started.md) explains how to use this to create a very simple app with a db, a backend +offering an API, and a frontend that displays something from the database. ### As a dependency @@ -28,8 +31,6 @@ Add it to your project as a dependency from clojars: - First check out the [conventions](./doc/conventions.md). - To start working with xiana, read the [tutorials](./doc/tutorials.md). -- A hands-on approach in the [how-to](./doc/How-To.md)s. -- Check the available [modules](./doc/modules.md), and [interceptors](./doc/interceptors.md). - To contribute, see the [contribution](./doc/contribution.md) docs. ### Examples @@ -45,5 +46,7 @@ Visit [examples folder](examples), to see how you can perform ## References ### Concept of interceptors + http://pedestal.io/reference/interceptors + https://github.com/metosin/sieppari diff --git a/deps.edn b/deps.edn index 0bfe8185..efc48263 100644 --- a/deps.edn +++ b/deps.edn @@ -39,13 +39,20 @@ {:extra-deps {clj-kondo/clj-kondo {:mvn/version "2022.05.31"}} :main-opts ["-m" "clj-kondo.main" "--lint" "src"]} - :codox {:extra-deps {codox/codox {:mvn/version "0.10.8"}} + :codox {:extra-deps {codox/codox {:mvn/version "0.10.8"}} :extra-paths ["resources"] - :exec-fn codox.main/generate-docs - :exec-args {:output-path "docs/new/" - :themes [:default :xiana] - :source-paths ["src"] - :doc-files ["doc/getting-started.md", "doc/How-To.md", "doc/Development-Guide.md"]}} + :exec-fn codox.main/generate-docs + :exec-args {:output-path "docs/new/" + :themes [:default :xiana] + :metadata {:doc/format :markdown} + :source-uri "https://github.com/Flexiana/framework/blob/{git-commit}/{filepath}#L{line}" + :source-paths ["src"] + :doc-files ["doc/welcome.md" + "doc/conventions.md" + "doc/tutorials.md" + "doc/how-to.md" + "doc/contribution.md" + "doc/getting-started.md"]}} :kibit {:extra-deps {tvaughan/kibit-runner {:mvn/version "1.0.1"}} diff --git a/doc/Development-Guide.md b/doc/Development-Guide.md deleted file mode 100644 index 68e12396..00000000 --- a/doc/Development-Guide.md +++ /dev/null @@ -1,27 +0,0 @@ -# Development guide -TODO fill this with everything a developer starting to work on Xiana might need to know - -* Ticket System -* Coding standards -* Submitting a PR -* Making a release - - -## Generating API Docs - -This is done automatically using *Codox* - -To generate or update the current version run the script: - -```shell -script/build-docs.sh -``` - -This runs the following: - -```shell -clj -X:codox -mv docs/new docs/{{version-number}} -``` - -It also updates the index.html file to point to the new version. diff --git a/doc/contribution.md b/doc/contribution.md index d9f218f9..0b72a75c 100644 --- a/doc/contribution.md +++ b/doc/contribution.md @@ -1,9 +1,37 @@ + + # Contribution +- [Ticket system](#ticket-system) +- [Coding standards](#coding-standards) +- [Submitting a PR](#submitting-a-pr) - [Development dependencies](#development-dependencies) - [Setup](#setup) - [Deps](#deps) - [Releasing](#releasing) +- [Generating API docs](#generating-api-docs) + +## Ticket system + +We're using GitHub [issues](https://github.com/Flexiana/framework/issues) for tracking and discussing ideas and +requests. + +## Coding standards + +Please follow `clj-style` and `kondo` instructions. `Kibit` isn't a showstopper, but PRs are more welcome if they don't +break `kibit`. + +## Submitting a PR + +Before you submit a PR be sure: + +- You've updated the documentation and the [CHANGELOG](../CHANGELOG.md) +- the PR has an issue in GitHub, with a good description +- you have added tests +- you provided an example project for a new feature + +All PRs need at least two approvals and pls +follow [Semantic Versioning 2.0.0](https://semver.org/#semantic-versioning-200) ## Development dependencies @@ -31,12 +59,8 @@ | nilenso/honeysql-postgres | 0.2.6 | PostGreSQL | | org.postgresql/postgresql | 42.2.2 | PostGreSQL | | crypto-password/crypto-password | 0.2.1 | Security | - -#### Optional - -| Name | Version | Provide | -|---------------------|---------|---------| -| clj-kondo/clj-kondo | RELEASE | Tests | +| clj-kondo/clj-kondo | RELEASE | Tests | +| npx | RELEASE | Documentation | ## Setup @@ -84,4 +108,38 @@ clj -M:install - Be sure all examples has the same framework version as it is in `release.edn` as dependency - Execute `./example-tests.sh` script. It will install the actual version of xiana, and go through the examples folder - for `check-style` and `lein test`. \ No newline at end of file + for `check-style` and `lein test`. + +## Generating API Docs + +This is done with [mermaid-cli](https://github.com/mermaid-js/mermaid-cli) and a forked version +of [Codox](https://github.com/Flexiana/codox). + +We're using mermaid-cli to render UML-diagrams in markdown files, see the `doc/conventions_template.md` for example. +These files need to be added to the `/script/build-docs.sh` . For using it you need to have `npx`. + +Codox is forked because markdown anchors aren't converted to HTML anchors in the official release. To use it you need + +```shell +git clone git@github.com:Flexiana/codox.git +cd codox/codox +lein install +``` + +it before generating the documentation. + +To generate or update the current version run the script: + +```shell +./script/build-docs.sh +``` + +This runs the following: + +```shell +npx -py @mermaid-js/mermaid-cli mmdc -i doc/conventions_template.md -o doc/conventions.md +clj -X:codox +mv docs/new docs/{{version-number}} +``` + +It also updates the index.html file to point to the new version. \ No newline at end of file diff --git a/doc/conventions-1.svg b/doc/conventions-1.svg new file mode 100644 index 00000000..4e7c6022 --- /dev/null +++ b/doc/conventions-1.svg @@ -0,0 +1 @@ +Web serverHandlerRouter interceptorsRouterController interceptorsActionEnhancing app state with requestEnter functionsMatching, routingLeave functions in reverted orderEnter functionsExecuting controllerLeave functions in reverted orderExtracting responseRequestStateStateStateStateStateStateStateResponseWeb serverHandlerRouter interceptorsRouterController interceptorsAction \ No newline at end of file diff --git a/doc/conventions-2.svg b/doc/conventions-2.svg new file mode 100644 index 00000000..9ee4bc7c --- /dev/null +++ b/doc/conventions-2.svg @@ -0,0 +1 @@ +HandlerInterceptor_IInterceptor_IIInterceptor_IIIAction:enter:enter:enterDo something:leave:leave:leaveStateStateStateStateStateStateStateStateHandlerInterceptor_IInterceptor_IIInterceptor_IIIAction \ No newline at end of file diff --git a/doc/conventions-3.svg b/doc/conventions-3.svg new file mode 100644 index 00000000..3141e7d6 --- /dev/null +++ b/doc/conventions-3.svg @@ -0,0 +1 @@ +HandlerInterceptor_IInterceptor_IIInterceptor_IIIAction:enter:enter:enter - Throwns an exception:error - Handles exception:leave:leave:error - Doesn't handles exception:error - Handles exception:error - Doesn't handles exception:error - Handles exception:leavealt[Interceptor_I handles the exception][Interceptor_II handles the exception]alt[Exception handled by thower][Exception not handled by thrower]Extract responseStateStateStateStateStateStateStateStateStateStateStateHandlerInterceptor_IInterceptor_IIInterceptor_IIIAction \ No newline at end of file diff --git a/doc/conventions.md b/doc/conventions.md index 8c6d50dc..104c5926 100644 --- a/doc/conventions.md +++ b/doc/conventions.md @@ -1,21 +1,29 @@ + + # Conventions +- [Overview](#overview) - [State](#state) - [Action](#action) - [Handler](#handler) - [Dependencies](#dependencies) - [Interceptors](#interceptors) +- [Interceptors error handling](#interceptors-error-handling) + +## Overview +The diagram bellow gives you an overview how a request is processed in Xiana based applications. +![diagram](./conventions-1.svg) ## State -A state record. It is created for each HTTP request and represents the current state of the application. It contains: +State is created for each HTTP request and represents the current state of the application. It contains: -- the application's dependencies +- the application's dependencies and configuration - request - request-data - response -This structure is very volatile, will be updated quite often on the application's life cycle. +This structure is very volatile, and will be updated quite often on the application's life cycle. The main modules that update the state are: @@ -25,7 +33,7 @@ The main modules that update the state are: - Interceptors: - Add, consumes or remove information from the state map. More details in [Interceptors](#interceptors) section. + Add, consume or remove information from the state map. More details in [Interceptors](#interceptors) section. - Actions: @@ -39,7 +47,7 @@ The state is renewed on every request. The action conventionally is the control point of the application flow. This is the place were you can define how the rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and -the additional side effect functions are you want to execute. +the additional side effect functions that you want to execute. Actions are defined in the routes vector @@ -49,20 +57,35 @@ Actions are defined in the routes vector ## Handler -Xiana's handler does all the processing. It runs on every request and does the following. It creates the state for every -request, matches the appropriate route, executes the interceptors, handles interceptor overrides, and not-found cases. +Xiana's handler creates the state for every request, matches the appropriate route, executes the interceptors, handles +interceptor overrides, and not-found cases. It handles websocket requests too. -### Routing +## Routing Routing means selecting the actions to execute depending on the request URL, and HTTP method. ## Dependencies -Modules can depend on external resources, configurations, as well as on other modules. These dependencies are added to +Modules can depend on external resources and configurations, as well as on other modules. These dependencies are added to the state on state creation, and defined on application startup. ## Interceptors -An interceptor is a pair of unary functions. Each function must recieve and return a state map. You can look at it as on an analogy to AOP's around aspect, or as on a pair of middlewares. They work mostly the same way as [pedestal](http://pedestal.io/reference/interceptors) and [sieppari](https://github.com/metosin/sieppari) interceptors. -Xiana provides a set of base [interceptors](interceptors.md), for the most common use cases. +An interceptor is a pair of unary functions. Each function must recieve and return a state map. Look at it as an analogy +to AOP's around aspect, or as on a pair of middlewares. They work mostly the same way +as [pedestal](http://pedestal.io/reference/interceptors) and [sieppari](https://github.com/metosin/sieppari) +interceptors. +Xiana provides a set of base interceptors, for the most common use cases. + +This figure shows how interceptors are executed ideally: +![diagram](./conventions-2.svg) +## Interceptors error handling: + +The interceptor executor handles the exceptional states like sieppari does. If an exception happens, it tries to handle +first in the same interceptor. If it has an `:error` handler, it will call it, otherwise it'll search for `:error` +handlers from the beginning of the interceptor queue. When an `:error` function found, and matched with the given +exception, the executor calls the queue of `:leave` functions in reserved order from where the handler was found. + +This diagram shows how the error cases are handled: +![diagram](./conventions-3.svg) \ No newline at end of file diff --git a/doc/conventions_template.md b/doc/conventions_template.md new file mode 100644 index 00000000..76841c49 --- /dev/null +++ b/doc/conventions_template.md @@ -0,0 +1,165 @@ + + +# Conventions + +- [Overview](#overview) +- [State](#state) +- [Action](#action) +- [Handler](#handler) +- [Dependencies](#dependencies) +- [Interceptors](#interceptors) +- [Interceptors error handling](#interceptors-error-handling) + +## Overview + +The diagram bellow gives you an overview how a request is processed in Xiana based applications. + +```mermaid + sequenceDiagram + Web server->>Handler:Request + Note over Handler: Enhancing app state with request + Handler->>Router interceptors:State + Note over Router interceptors: Enter functions + Router interceptors->>Router: State + Note over Router:Matching, routing + Router->>Router interceptors:State + Note over Router interceptors: Leave functions in reverted order + Router interceptors->>Controller interceptors: State + Note over Controller interceptors: Enter functions + Controller interceptors->>Action: State + Note over Action:Executing controller + Action->>Controller interceptors:State + Note over Controller interceptors: Leave functions in reverted order + Controller interceptors->>Handler:State + Note over Handler:Extracting response + Handler->>Web server:Response +``` + +## State + +State is created for each HTTP request and represents the current state of the application. It contains: + +- the application's dependencies and configuration +- request +- request-data +- response + +This structure is very volatile, and will be updated quite often on the application's life cycle. + +The main modules that update the state are: + +- Routes: + + Add information from the matched route to the state map + +- Interceptors: + + Add, consume or remove information from the state map. More details in [Interceptors](#interceptors) section. + +- Actions: + + In actions, you are able to interfere with the :leave parts of the interceptors. + +At the last step of execution the handler extracts the response value from the state. + +The state is renewed on every request. + +## Action + +The action conventionally is the control point of the application flow. This is the place were you can define how the +rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and +the additional side effect functions that you want to execute. + +Actions are defined in the routes vector + +```clojure +["/" {:get {:action #(do something)}}] +``` + +## Handler + +Xiana's handler creates the state for every request, matches the appropriate route, executes the interceptors, handles +interceptor overrides, and not-found cases. +It handles websocket requests too. + +## Routing + +Routing means selecting the actions to execute depending on the request URL, and HTTP method. + +## Dependencies + +Modules can depend on external resources and configurations, as well as on other modules. These dependencies are added to +the state on state creation, and defined on application startup. + +## Interceptors + +An interceptor is a pair of unary functions. Each function must recieve and return a state map. Look at it as an analogy +to AOP's around aspect, or as on a pair of middlewares. They work mostly the same way +as [pedestal](http://pedestal.io/reference/interceptors) and [sieppari](https://github.com/metosin/sieppari) +interceptors. +Xiana provides a set of base interceptors, for the most common use cases. + +This figure shows how interceptors are executed ideally: + +```mermaid + sequenceDiagram + Handler->>Interceptor_I:State + Note over Interceptor_I: :enter + Interceptor_I->>Interceptor_II:State + Note over Interceptor_II: :enter + Interceptor_II->>Interceptor_III:State + Note over Interceptor_III: :enter + Interceptor_III->>Action:State + Note over Action: Do something + Action->>Interceptor_III:State + Note over Interceptor_III::leave + Interceptor_III->>Interceptor_II:State + Note over Interceptor_II::leave + Interceptor_II->>Interceptor_I:State + Note over Interceptor_I::leave + Interceptor_I->>Handler:State +``` + +## Interceptors error handling: + +The interceptor executor handles the exceptional states like sieppari does. If an exception happens, it tries to handle +first in the same interceptor. If it has an `:error` handler, it will call it, otherwise it'll search for `:error` +handlers from the beginning of the interceptor queue. When an `:error` function found, and matched with the given +exception, the executor calls the queue of `:leave` functions in reserved order from where the handler was found. + +This diagram shows how the error cases are handled: + +```mermaid +sequenceDiagram +Handler->>Interceptor_I:State +Note over Interceptor_I: :enter +Interceptor_I->>Interceptor_II:State +Note over Interceptor_II: :enter +Interceptor_II->>Interceptor_III:State +Note over Interceptor_III: :enter - Throwns an exception +alt Exception handled by thower + Note over Interceptor_III: :error - Handles exception + Interceptor_III->>Interceptor_II:State + Note over Interceptor_II::leave + Interceptor_II->>Interceptor_I:State + Note over Interceptor_I::leave + Interceptor_I->>Handler:State + +else Exception not handled by thrower + Note over Interceptor_III: :error - Doesn't handles exception + Interceptor_III->>Interceptor_I:State + alt Interceptor_I handles the exception + Note over Interceptor_I: :error - Handles exception + Interceptor_I->>Handler:State + else Interceptor_II handles the exception + Note over Interceptor_I: :error - Doesn't handles exception + Interceptor_I->>Interceptor_II:State + Note over Interceptor_II: :error - Handles exception + Interceptor_II->>Interceptor_I:State + Note over Interceptor_I::leave + Interceptor_I->>Handler:State + end +end + Note over Handler:Extract response +participant Action +``` diff --git a/doc/decisions/0001-record-architecture-decisions.md b/doc/decisions/0001-record-architecture-decisions.md deleted file mode 100644 index 97f4ba88..00000000 --- a/doc/decisions/0001-record-architecture-decisions.md +++ /dev/null @@ -1,19 +0,0 @@ -# 1. Record architecture decisions - -Date: 2021-01-12 - -## Status - -Accepted - -## Context - -We need to record the architectural decisions made on this project. - -## Decision - -We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). - -## Consequences - -See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). diff --git a/doc/decisions/0002-database-basic-architecture.md b/doc/decisions/0002-database-basic-architecture.md deleted file mode 100644 index 92c555c4..00000000 --- a/doc/decisions/0002-database-basic-architecture.md +++ /dev/null @@ -1,39 +0,0 @@ -# 2. Database basic architecture - -Date: 2021-01-14 - -## Status - -Proposed - -## Context - -1. The framework rationale requires a separation of concern between "code -is code, data is data" and because of that some libraries were not -able to proceed to be used in the application. - -2. The framework also wants to be a complete solution which means that -our users should not be required to learn too many libraries in order -to be productive. Instead, the framework should provide layers of -indirection to wrap such functionalities and provide centralized -control over the features. - -## Decision - -- Usage of `yogthos/config` to handle config files -- Usage of `stuartsierra/components` to handle dependency management -- Usage of `honeysql` to handle SQL interactions -- Build a framework layer to handle migrations in `honeysql` style -- Build framework layer to wrap honeysql, honeysql-plugins, next-jdbc, - hikari-cp and possible others lirbaries. - -## Consequences - -- [Positive] Framework is more aligned with its goals -- [Positive] Framework already have a `SQL` layer to be used -- [Negative] Choosing to wrap underlying libraries adds the work to - keep our library in sync with new features they release -- [Negative] We were not able to use `integrant` as desired in the - beginning because it violates the first rationale. -- [Negative] We were not able to use `duct` as desired because it - violates the first rationale diff --git a/doc/decisions/0003-controller-architecture.md b/doc/decisions/0003-controller-architecture.md deleted file mode 100644 index 7234ea24..00000000 --- a/doc/decisions/0003-controller-architecture.md +++ /dev/null @@ -1,153 +0,0 @@ -# 3. Controller basic architecture - -Date: 2021-01-27 - -## Staus - -Proposed - -## Context - 1. The Controller is responsible for the lifecycle of the framework. It reflects the starting point and the means of controll of all the needed actions that the framework must do (load deps, render views, generate routes etc). - 2. The framework requires a simple and easy controll to the end user. - 3. In turn the interaction of the user with the controller should be simple understandable and destructured in smaller steps that would provide a better grain of detail in the controll of the application, validation and error management etc. - 4. The framework rational requires a functional approach but in a way that would be easy for new users and in a way that it would use user's previous concept knowledge from similar frameworks in other programming languages. - -## Decisions -### 1. Controller being `monadic` -#### Explanation: -The Controller would be a composition of different functions in steps as was created in the initial draft by Lukáš Rychtecký. -#### Why: -This would provide the user with the ability to controll each -step on it's creation. -##### Example: - -``` clojure -(defn controller - [] - (-> {} - (set-routes) - (set-template) - (set-response ) - (set-middlewares ) - (set-actions ) - (set-deps) - (set-session)) - ) -``` -### 2. Controller use a State map. -#### Explanation: -The Controller would be 'feeded' with an initial state-map that the user would provide. The state-map would hold the data that would be needed to build the Controller. This approach is similar to the State monad but it would return only the result of the configuration and discard the initial state. -#### Why: -This helps in two ways: -1. We can enforce a specific stucture in the definition of the state-map to the user. This would give a clearer understanding on the 'ingredients' that a controller would need to created to the user, as it would be a map which its keys would represent the different stages of the controller. -2. We would be able to validate the state-map and its data. -##### Example: -###### state map: -``` clojure -(def state {:http-request {:method :get - :url "/hi" - :request {:action :select :params {}}} - :response {:headers (list {"Content-Type" "txt/html"} - {"charset" "UTF-8"}) - :body nil} - :request-data {:model Foo - :template (comp temp) - :middlewares []} - :session-data {} - :deps {}}) -``` - -###### state map schema: -``` clojure -;; mali schema for the State Map -(def State-map - [:map - [:http-request [:map - [:method [:enum :get :post :put]] - [:url [:vector]] - [:request [:action [:enum :select :update :delet :insert]]]]] - [:response [:map - [:headers [:vector]] - [:body [:or [:string] [:vector] [:nil]]] - ]] - [:request-data [:map - [:model ;;malli schema - [:vector]] - [:template ;; hiccup or static - [:or [:vector] - [:string]]] - [:middlewares ;; list of functions to used for the middleware - [:vector]]]] - [:deps [:map]] - [:session [:map]]]) -``` - -###### controller with state-map: - -``` clojure -(defn controller - [state-map] - (-> {} - (set-routes state-map) - (set-template state-map) - (set-response state-map) - (set-middlewares state-map) - (set-actions state-map) - (set-deps state-map) - (set-session state-map)) - ) -``` - -### Using a metosin/reitit wrapper -#### Explanation: -The controller would be wraped in a thin reitit wrapper with initial approach. -#### Why: -1. Elevates the use of reitit and its already implemented functionality and safeguards. -2. Availability to perform validation on map to gurantee the correct definition of each stage. -##### Exapmle: -###### controller result map - -``` clojure -{:route ["/hi" :get], - :template #function[poc.core/temp], - :response - {:headers ({"Content-Type" "txt/html"} {"charset" "UTF-8"}), :body nil}, - :middlewares [], - :actions {:action :select, :params {}}, - :deps {}, - :session nil} -``` -###### result map schema - -``` clojure -;; internal schema of an instance of a state-map -(def instance-map - [:map - [:route [:vector]] - [:respones [:map [:headers [:list] - :body [:or [:string] [:vector]]]]] - [:template [:or [:string] [:vector]]] - [:actions [:vector [:and [:enum :select :delete :update :insert] [:map]]]] - [:deps [:map]] - [:middlewares [:list]] - [:session [:map]]]) -;; -``` -###### controller build and routes - -``` clojure -(defn build-controller - [ctrl] - ) - -(defn generate-routes - [ctrl] - (ring/ring-handler - (ring/router - ["/api" - (:route ctrl)] - {:data {:coercion reitit.coercion.spec/coercion - :middleware [rrc/coerce-exceptions-middleware - rrc/coerce-request-middleware - rrc/coerce-response-middleware]}}))) -``` diff --git a/doc/getting-started.md b/doc/getting-started.md index 7601db94..c0831057 100755 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -1,3 +1,5 @@ + + # How to make a Todo app using Xiana ## Requirements @@ -27,13 +29,15 @@ lein shadow npm-deps ### 3. Run dockerized database -You need to have docker and docker-compose installed on your machine for the following command to work. Run the following from the root directory of your project. +You need to have docker and docker-compose installed on your machine for the following command to work. Run the +following from the root directory of your project. ```bash docker-compose up -d ``` -This should spin up a PostgreSQL database on port 5433. Name of the DB is `todo_app` You can verify that the database is running by connecting to it. Value of `username` and `password` is *`postgres`.* +This should spin up a PostgreSQL database on port 5433. Name of the DB is `todo_app` You can verify that the database is +running by connecting to it. Value of `username` and `password` is *`postgres`.* ```bash docker exec -it todo-app_db_1 psql -U postgres -d todo_app @@ -43,7 +47,8 @@ which should open a PostgreSQL shell if successful. ### 4. Populate database with data -On application start, the framework will look for database migrations located in the configured location. By default, this location is set to *resources/migrations* directory*.* +On application start, the framework will look for database migrations located in the configured location. By default, +this location is set to *resources/migrations* directory*.* It is possible to create migrations by running from the project directory @@ -51,7 +56,8 @@ It is possible to create migrations by running from the project directory lein migrate create todos ``` -This will create two migration files inside the *resources/migrations* directory. Both file names will be prefixed by the timestamp value of the file creation. +This will create two migration files inside the *resources/migrations* directory. Both file names will be prefixed by +the timestamp value of the file creation. Put the following SQL code inside the *todos-up.sql* file @@ -65,7 +71,9 @@ CREATE TABLE todos done boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now() ); + --;; + INSERT INTO todos (label) values ('example label from DB'); ``` @@ -80,18 +88,19 @@ DROP TABLE todos; 1. Run Clojure REPL and load contents of file *dev/user.clj* into it. 2. Execute function *start-dev-system* 3. Your **Todo App** should now be running. You can verify it by visiting URL -*[http://localhost:3000/re-frame](http://localhost:3000/re-frame)* in your browser. + *[http://localhost:3000/re-frame](http://localhost:3000/re-frame)* in your browser. ### 6. Add endpoint to backend API -Routes definition can be found in the *src/backend/todo_app/core.clj* file. Replace the routes definition with following code. +Routes definition can be found in the *src/backend/todo_app/core.clj* file. Replace the routes definition with following +code. ```clojure (def routes [["/api/todos" {:action #'fetch}]]) ``` -define a function to be called when the endpoint is hit. +define a function to be called when the endpoint is hit. ```clojure (defn fetch @@ -99,7 +108,8 @@ define a function to be called when the endpoint is hit. state) ``` -Now you need to reload the changed files to REPL and restart the application by executing *start-dev-system* function once again. +Now you need to reload the changed files to REPL and restart the application by executing *start-dev-system* function +once again. Now opening [http://localhost:3000/api/todos](http://localhost:3000/api/todos) in your browser returns a blank page. ### 7. Hello World! @@ -110,14 +120,14 @@ Change the implementation of *fetch* function to display "Hello World!" every ti (defn fetch [state] (assoc state :response {:status 200 - :body "Hello World!"})) + :body "Hello World!"})) ``` Reload the modified function and restart the application. ### 8. Return contents of the todos table -Change the implementation of function *fetch* once again and create a new *view* function. +Change the implementation of function *fetch* once again and create a new *view* function. Function: @@ -128,15 +138,15 @@ Function: (defn view [{{db-data :db-data} :response-data :as state}] (assoc state :response {:status 200 - :body (mapv :todos/label db-data)})) + :body (mapv :todos/label db-data)})) (defn fetch [state] (assoc state - :view view - :query {:select [:*] :from [:todos]})) + :view view + :query {:select [:*] :from [:todos]})) ``` -Again, reload the modified function and restart the application. +Again, reload the modified function and restart the application. After running following curl command from your shell, live data from your database should appear on your screen. @@ -146,7 +156,7 @@ curl http://localhost:3000/api/todos ## Create a page in the app to display data returned by the endpoint -### 1. Add frontend dependencies +### 1. Add frontend dependencies Add following dependencies into *project.clj* file. @@ -154,18 +164,19 @@ Add following dependencies into *project.clj* file. (defproject ... :dependencies [ - ... - [cljs-ajax "0.8.4"] - [day8.re-frame/http-fx "0.2.3"] - ... - ] + ... + [cljs-ajax "0.8.4"] + [day8.re-frame/http-fx "0.2.3"] + ... + ] ... ) ``` ### 2. Set initial value of re-frame database -File *src/frontend/todo_app/db.cljs* contains initial value of re-frame databse. Inside of this file replace value of *default-db* to following: +File *src/frontend/todo_app/db.cljs* contains initial value of re-frame databse. Inside of this file replace value of * +default-db* to the following: ```clojure (def default-db @@ -179,39 +190,39 @@ Replace contents of *src/frontend/todo_app/events.cljs* file with the following ```clojure (ns todo-app.events (:require - [re-frame.core :as re-frame] - [ajax.core :as ajax] - [day8.re-frame.http-fx] - [todo-app.db :as db] - )) + [re-frame.core :as re-frame] + [ajax.core :as ajax] + [day8.re-frame.http-fx] + [todo-app.db :as db] + )) (defn url [tail] (str "http://localhost:3000" tail)) (re-frame/reg-event-db - ::initialize-db - (fn [_ _] - db/default-db)) + ::initialize-db + (fn [_ _] + db/default-db)) (re-frame/reg-event-db - ::add-todos->db - (fn [db [_ response]] - (assoc db :todos response))) + ::add-todos->db + (fn [db [_ response]] + (assoc db :todos response))) (re-frame/reg-event-db - ::failure - (fn [db _] - (js/console.error "Something is wrong!") - db)) + ::failure + (fn [db _] + (js/console.error "Something is wrong!") + db)) (re-frame/reg-event-fx - ::fetch-todos! - (fn [_ [_]] - (js/console.info "Fetching todos!") - {:http-xhrio {:uri (url "/api/todos") - :response-format (ajax/json-response-format {:keywords? true}) - :format (ajax/json-request-format) - :on-success [::add-todos->db] - :on-failure [::failure]}})) + ::fetch-todos! + (fn [_ [_]] + (js/console.info "Fetching todos!") + {:http-xhrio {:uri (url "/api/todos") + :response-format (ajax/json-response-format {:keywords? true}) + :format (ajax/json-request-format) + :on-success [::add-todos->db] + :on-failure [::failure]}})) ``` ### 4. Define re-frame subscription @@ -221,7 +232,7 @@ Replace contents of *src/frontend/todo_app/subs.cljs* file with the following co ```clojure (ns todo-app.subs (:require - [re-frame.core :as re-frame])) + [re-frame.core :as re-frame])) (re-frame/reg-sub ::todos @@ -236,16 +247,16 @@ Replace contents of *src/frontend/todo_app/views.cljs* file with the following c ```clojure (ns todo-app.views (:require - [re-frame.core :as re-frame] - [todo-app.events :as events] - [todo-app.subs :as subs])) + [re-frame.core :as re-frame] + [todo-app.events :as events] + [todo-app.subs :as subs])) (defn main-panel [] (re-frame/dispatch [::events/fetch-todos!]) - (let [todos (re-frame/subscribe [::subs/todos])] - [:div - (map #(identity [:ul %]) - @todos)])) + (let [todos (re-frame/subscribe [::subs/todos])] + [:div + (map #(identity [:ul %]) + @todos)])) ``` ### 6. Define frontend routes @@ -262,9 +273,11 @@ Update *routes* definition in file *src/backend/todo_app/core.clj* to following: ### 7. At this point the app should be running -Again, reload the modified code and restart the application. After opening [http://localhost:3000/todos](http://localhost:3000/todos) in your browser, it returns a page containing the data from our database that looks like this: +Again, reload the modified code and restart the application. After +opening [http://localhost:3000/todos](http://localhost:3000/todos) in your browser, it returns a page containing the +data from our database that looks like this: -![success](../resources/images/success.png) +![success](./resources/images/success.png) ## Create another endpoint to add a new entry to the DB diff --git a/doc/How-To.md b/doc/how-to.md similarity index 69% rename from doc/How-To.md rename to doc/how-to.md index 4bd37526..a90cc5b1 100644 --- a/doc/How-To.md +++ b/doc/how-to.md @@ -1,305 +1,175 @@ + + # How to -- [Login implementation](#login-implementation) -- [Logout implementation](#logout-implementation) -- [Session management](#session-management) +- [Defining new interceptors](#defining-new-interceptors) + - [Interceptor example](#interceptor-example) +- [Providing default interceptors](#providing-default-interceptors) +- [Interceptor overriding](#interceptor-overriding) +- [Role based access and data ownership control](#role-based-access-and-data-ownership-control) - [Access and data ownership control](#access-and-data-ownership-control) - [Role set definition](#role-set-definition) - [Provide resource/action at routing](#provide-resourceaction-at-routing) - [Application start-up](#application-start-up) - [Access control](#access-control) - [Data ownership](#data-ownership) - - [All together](#all-together) +- [Login implementation](#login-implementation) +- [Logout implementation](#logout-implementation) -## Login implementation +## Defining new interceptors -Xiana framework does not have any login or logout functions, as every application has its own user management logic. -Though Xiana offers all the tools to easily implement them. One of the default interceptors is the session interceptor. -If included, it can validate a request only if the session already exists in session storage. To log in a user, simply -add its session data to the storage. (TODO: where? What is the exact key to modify?). All sessions should have a unique -UUID as session-id. The active session lives under `(-> state :session-data)`. On every request, before reaching the -action defined by the route, the interceptor checks `[:headers :session-id]` among other things. Which is the id of the -current session. The session is then loaded in session storage. If the id is not found, the execution flow is -interrupted with the response: +The interceptor is a map, can have three functions like: -```clojure -{:status 401 - :body "Invalid or missing session"} -``` +`:enter` Runs while we are going down from the request to it's action, in the order of executors -To implement login, you need to [use the session interceptor](tutorials.md#interceptor-overriding) in +`:leave` Runs while we're going up from the action to the response. -```clojure -(let [;; Create a unique ID - session-id (UUID/randomUUID)] - ;; Store a new session in session storage - (add! session-storage session-id {:session-id session-id}) - ;; Make sure session-id is part of the response - (assoc-in state [:response :headers :session-id] (str session-id))) -``` +`:error` Executed when any error thrown while executing the two other functions -or use the `guest-session` interceptor, which creates a guest session for unknown, or missing sessions. +and a `:name` can be defined. All keys are optional, and if it missing it's replaced by `identity`. -For role-based access control, you need to store the actual user in your session data. First, you'll have to query it -from the database. It is best placed in models/user namespace. Here's an example: +The provided functions are should have one parameter, the application state, and should return the modified state. -```clojure -(defn fetch-query - [state] - (let [login (-> state :request :body-params :login)] - (-> (select :*) - (from :users) - (where [:and - :is_active - [:or - [:= :email login] - [:= :username login]]])))) -``` - -To execute it, place `db-access` interceptor in the interceptors list. It injects the query result into the state. If -you already have this injected, you can modify your create session function like this: +### Interceptor example ```clojure -(let [;; Get user from database result - user (-> state :response-data :db-data first) - ;; Create session - session-id (UUID/randomUUID)] - ;; Store the new session in session storage. Notice the addition of user. - (add! session-storage session-id (assoc user :session-id session-id)) - ;; Make sure session-id is part of the response - (assoc-in state [:response :headers :session-id] (str session-id))) -``` - -Be sure to remove user's password and any other sensitive information before storing it: -```clojure -(let [;; Get user from database result - user (-> state - :response-data - :db-data - first - ;; Remove password for session storage - (dissoc :users/password)) - ;; Create session id - session-id (UUID/randomUUID)] - ;; Store the new session in session storage - (add! session-storage session-id (assoc user :session-id session-id)) - ;; Make sure session-id is part of the response - (assoc-in state [:response :headers :session-id] (str session-id))) +{:name :sample-interceptor + :enter (fn [state] + (println "Enter: " state) + (-> state + (transform-somehow) + (or-do-side-effects))) + :leave (fn [state] + (println "Leave: " state) + state) + :error (fn [state] + (println "Error: " state) + ;; Here `state` should have previously thrown exception + ;; stored in `:error` key. + ;; you can do something useful with it (e.g. log it) + ;; and/or handle it by `dissoc`ing from the state. + ;; In that case remaining `leave` interceptors will be executed. + (assoc state :response {:status 500 :body "Error occurred while printing out state"}))} ``` -Next, we check if the credentials are correct, so we use an `if` statement. +## Providing default interceptors -```clojure -(if (valid-credentials?) - (let [;; Get user from database result - user (-> state - :response-data - :db-data - first - ;; Remove password for session storage - (dissoc :users/password)) - ;; Create session ID - session-id (UUID/randomUUID)] - ;; Store the new session in session storage - (add! session-storage session-id (assoc user :session-id session-id)) - ;; Make sure session-id is part of the response - (assoc-in state [:response :headers :session-id] (str session-id))) - (throw (ex-info "Missing session data" - {:xiana/response - {:body "Login failed" - :status 401}}))) -``` - -Xiana provides `xiana.hash` to check user credentials: +The router and controller interceptors definition is part of the application startup. The system's dependency map should +contain two sequence of interceptors like ```clojure -(defn- valid-credentials? - "It checks that the password provided by the user matches the encrypted password from the database." - [state] - (let [user-provided-pass (-> state :request :body-params :password) - db-stored-pass (-> state :response-data :db-data first :users/password)] - (and user-provided-pass - db-stored-pass - (hash/check state user-provided-pass db-stored-pass)))) +(def app-cfg + {:routes routes + :router-interceptors [...] + :controller-interceptors [...] + :web-socket-interceptors [...]}) ``` -The login logic is done, but where to place it? +## Interceptor overriding -Do you remember the [side effect interceptor](interceptors.md#side-effect)? It's running after we have the query result -from the database, and before the final response is rendered with the [view interceptor](interceptors.md#view). The -place for the function defined above is in the interceptor chain. How does it go there? Let's see -an [action](conventions.md#action) +On route definition you can interfere with the default controller interceptors. With the route definition you are able +to set up different controller interceptors other than the ones already defined with the app. There are three ways to do +it: ```clojure -(defn action - [state] - (assoc state :side-effect side-effects/login)) +... {:action #(do something) + :interceptors [...]} ``` -This is the place for injecting the database query, too: +will override all controller interceptors ```clojure -(defn action - [state] - (assoc state :side-effect side-effects/login - :query model/fetch-query)) +... {:action #(do something) + :interceptors {:around [...]}} ``` -But some tiny thing is still missing. The definition of the response in the all-ok case. A happy path response. +will extend the defaults around ```clojure -(defn login-success - [state] - (let [id (-> state :response-data :db-data first :users/id)] - (-> state - (assoc-in [:response :body] - {:view-type "login" - :data {:login "succeed" - :user-id id}}) - (assoc-in [:response :status] 200)))) +... {:action #(do something) + :interceptors {:inside [...]}} ``` -And finally the [view](tutorials.md#view) is injected in the action function: +will extend the defaults inside ```clojure -(defn action - [state] - (assoc state :side-effect side-effects/login - :view view/login-success - :query model/fetch-query)) +... {:action #(do something) + :interceptors {:inside [...] + :around [...]}} ``` -## Logout implementation - -To do a logout is much easier than a login implementation. The `session-interceptor` does half of the work, and if you -have a running session, then it will not complain. The only thing you should do is to remove the actual session from -the `state` -and from session storage. Something like this: +will extend the defaults inside and around ```clojure -(defn logout - [state] - (let [session-store (get-in state [:deps :session-backend]) - session-id (get-in state [:session-data :session-id])] - (session/delete! session-store session-id) - (dissoc state :session-data))) +... {:action #(do something) + :interceptors {:except [...]}} ``` -Add the `ok` response +will skip the excepted interceptors from defaults -```clojure -(defn logout-view - [state] - (-> state - (assoc-in [:response :body] - {:view-type "logout" - :data {:logout "succeed"}}) - (assoc-in [:response :status] 200))) -``` +## Role based access and data ownership control -and use it: +To get the benefits of [tiny RBAC](https://github.com/Flexiana/tiny-rbac) library you need to provide the resource and +the action for your endpoint in [router](#routes) definition: ```clojure -(defn logout - [state] - (let [session-store (get-in state [:deps :session-backend]) - session-id (get-in state [:session-data :session-id])] - (session/delete! session-store session-id) - (-> state - (dissoc :session-data) - (assoc :view views/logout-view)))) +[["/api" + ["/image" {:delete {:action delete-action + :permission :image/delete}}]]] ``` -## Session management - -Session management is done via two components - -- session backend, which can be - - in-memory - - persistent -- session interceptors - -### In memory session backend - -Basically it's an atom backed session protocol implementation, allows you to `fetch` `add!` `delete!` `dump` -and `erase!` session data, or the whole session storage. It doesn't require any additional configuration, and this is -the default set up for handling session storage. All stored session data is wiped out on system restart. - -### Persistent session backend - -Instead of atom, it uses a postgresql table to store session data. Has the same protocol as in-memory. Configuration is -necessary to use it. - -- it's necessary to have a table in postgres: +and add your role-set into your app's [dependencies](#dependencies-and-configuration): -```postgres-sql -CREATE TABLE sessions ( - session_data json not null, - session_id uuid primary key, - modified_at timestamp DEFAULT CURRENT_TIMESTAMP -); +```clojure +(defn ->system + [app-cfg] + (-> (config/config app-cfg) + xiana.rbac/init + ws/start)) ``` -- you need to define the session's configuration in you `config.edn` files: +On `:enter`, the interceptor performs the permission check. It determines if the action allowed for the user found +in `(-> state :session-data :user)`. If access to the resource/action isn't permitted, then the response is: ```clojure - :xiana/session-backend {:storage :database - :session-table-name :sessions} +{:status 403 + :body "Forbidden"} ``` -- in case of - - missing `:storage` key, `in-memory` session backend will be used - - missing `:session-table-name` key, `:sessions` table will be used +If a permission is found, then it goes into `(-> state :request-data :user-permissions)` as a parameter for data +ownership processing. -- the database connection can be configured in three ways: +On `:leave`, executes the restriction function found in `(-> state :request-data :restriction-fn)`. The `restriction-fn` +should look like this: - In resolution order - - via additional configuration - ```clojure - :xiana/session-backend {:storage :database - :session-table-name :sessions - :port 5433 - :dbname "app-db" - :host "localhost" - :dbtype "postgresql" - :user "db-user" - :password "db-password"} - ``` - - using the same datasource as the application use: - - Just init the backend after the database connection - ```clojure - (defn ->system - [app-cfg] - (-> (config/config app-cfg) - routes/reset - db-core/connect - db-core/migrate! - session/init-backend - ws/start)) - ``` - - Creating new datasource - - If no datasource is provided on initialization, the `init-backend` function merges the database config with the - session backend configuration, and creates a new datasource from the result. - -### Session interceptors +```clojure +(defn restriction-fn + [state] + (let [user-permissions (get-in state [:request-data :user-permissions])] + (cond + (user-permissions :image/all) state + (user-permissions :image/own) (let [session-id (get-in state [:request :headers "session-id"]) + session-backend (-> state :deps :session-backend) + user-id (:users/id (session/fetch session-backend session-id))] + (update state :query sql/merge-where [:= :owner.id user-id]))))) +``` -[See interceptors](interceptors.md#session) +The rbac interceptor must be placed between the [action](#action) and the [db-access](#database-access) interceptors in +the interceptor chain. ## Access and data ownership control -[RBAC](tutorials.md#role-based-access-and-data-ownership-control) is a handy way to restrict user actions on different +[RBAC](./tutorials.md#role-based-access-and-data-ownership-control) is a handy way to restrict user actions on different resources. It's a role-based access control and helps you to implement data ownership control. The `rbac/interceptor` -should be placed [inside](tutorials.md#interceptor-overriding) [db-access](interceptors.md#db-access). +should be placed [inside](./tutorials.md#interceptor-overriding) [db-access](./interceptors.md#db-access). ### Role set definition For [tiny-RBAC](https://github.com/Flexiana/tiny-rbac) you should provide a [role-set](https://github.com/Flexiana/tiny-rbac#builder). It's a map which defines the application resources, the actions on it, the roles with the different granted actions, and restrictions for data ownership control. This map must -be placed in [deps](conventions.md#dependencies). +be placed in [deps](./conventions.md#dependencies). Here's an example role-set for an image service: @@ -467,7 +337,208 @@ And finally, the only missing piece of code: the model, and the action (defn delete-image [state] + (-> state + (assoc :query (delete-query state)) + (assoc-in [:request-data :restriction-fn] restriction-fn))) +``` + +## Login implementation + +Xiana framework does not have any login or logout functions, as every application has its own user management logic. +Though Xiana offers all the tools to easily implement them. One of the default interceptors is the session interceptor. +If included, it can validate a request only if the session already exists in session storage. To log in a user, simply +add its session data to the storage. (TODO: where? What is the exact key to modify?). All sessions should have a unique +UUID as session-id. The active session lives under `(-> state :session-data)`. On every request, before reaching the +action defined by the route, the interceptor checks `[:headers :session-id]` among other things. Which is the id of the +current session. The session is then loaded in session storage. If the id is not found, the execution flow is +interrupted with the response: + +```clojure +{:status 401 + :body "Invalid or missing session"} +``` + +To implement login, you need to [use the session interceptor](tutorials.md#interceptor-overriding) in + +```clojure +(let [;; Create a unique ID + session-id (UUID/randomUUID)] + ;; Store a new session in session storage + (add! session-storage session-id {:session-id session-id}) + ;; Make sure session-id is part of the response + (assoc-in state [:response :headers :session-id] (str session-id))) +``` + +or use the `guest-session` interceptor, which creates a guest session for unknown, or missing sessions. + +For role-based access control, you need to store the actual user in your session data. First, you'll have to query it +from the database. It is best placed in models/user namespace. Here's an example: + +```clojure +(defn fetch-query + [state] + (let [login (-> state :request :body-params :login)] + (-> (select :*) + (from :users) + (where [:and + :is_active + [:or + [:= :email login] + [:= :username login]]])))) +``` + +To execute it, place `db-access` interceptor in the interceptors list. It injects the query result into the state. If +you already have this injected, you can modify your create session function like this: + +```clojure +(let [;; Get user from database result + user (-> state :response-data :db-data first) + ;; Create session + session-id (UUID/randomUUID)] + ;; Store the new session in session storage. Notice the addition of user. + (add! session-storage session-id (assoc user :session-id session-id)) + ;; Make sure session-id is part of the response + (assoc-in state [:response :headers :session-id] (str session-id))) +``` + +Be sure to remove user's password and any other sensitive information before storing it: + +```clojure +(let [;; Get user from database result + user (-> state + :response-data + :db-data + first + ;; Remove password for session storage + (dissoc :users/password)) + ;; Create session id + session-id (UUID/randomUUID)] + ;; Store the new session in session storage + (add! session-storage session-id (assoc user :session-id session-id)) + ;; Make sure session-id is part of the response + (assoc-in state [:response :headers :session-id] (str session-id))) +``` + +Next, we check if the credentials are correct, so we use an `if` statement. + +```clojure +(if (valid-credentials?) + (let [;; Get user from database result + user (-> state + :response-data + :db-data + first + ;; Remove password for session storage + (dissoc :users/password)) + ;; Create session ID + session-id (UUID/randomUUID)] + ;; Store the new session in session storage + (add! session-storage session-id (assoc user :session-id session-id)) + ;; Make sure session-id is part of the response + (assoc-in state [:response :headers :session-id] (str session-id))) + (throw (ex-info "Missing session data" + {:xiana/response + {:body "Login failed" + :status 401}}))) +``` + +Xiana provides `xiana.hash` to check user credentials: + +```clojure +(defn- valid-credentials? + "It checks that the password provided by the user matches the encrypted password from the database." + [state] + (let [user-provided-pass (-> state :request :body-params :password) + db-stored-pass (-> state :response-data :db-data first :users/password)] + (and user-provided-pass + db-stored-pass + (hash/check state user-provided-pass db-stored-pass)))) +``` + +The login logic is done, but where to place it? + +Do you remember the [side effect interceptor](./interceptors.md#side-effect)? It's running after we have the query +result +from the database, and before the final response is rendered with the [view interceptor](./interceptors.md#view). The +place for the function defined above is in the interceptor chain. How does it go there? Let's see +an [action](./conventions.md#action) + +```clojure +(defn action + [state] + (assoc state :side-effect side-effects/login)) +``` + +This is the place for injecting the database query, too: + +```clojure +(defn action + [state] + (assoc state :side-effect side-effects/login + :query model/fetch-query)) +``` + +But some tiny thing is still missing. The definition of the response in the all-ok case. A happy path response. + +```clojure +(defn login-success + [state] + (let [id (-> state :response-data :db-data first :users/id)] (-> state - (assoc :query (delete-query state)) - (assoc-in [:request-data :restriction-fn] restriction-fn))) + (assoc-in [:response :body] + {:view-type "login" + :data {:login "succeed" + :user-id id}}) + (assoc-in [:response :status] 200)))) +``` + +And finally the [view](tutorials.md#view) is injected in the action function: + +```clojure +(defn action + [state] + (assoc state :side-effect side-effects/login + :view view/login-success + :query model/fetch-query)) +``` + +## Logout implementation + +To do a logout is much easier than a login implementation. The `session-interceptor` does half of the work, and if you +have a running session, then it will not complain. The only thing you should do is to remove the actual session from +the `state` +and from session storage. Something like this: + +```clojure +(defn logout + [state] + (let [session-store (get-in state [:deps :session-backend]) + session-id (get-in state [:session-data :session-id])] + (session/delete! session-store session-id) + (dissoc state :session-data))) +``` + +Add the `ok` response + +```clojure +(defn logout-view + [state] + (-> state + (assoc-in [:response :body] + {:view-type "logout" + :data {:logout "succeed"}}) + (assoc-in [:response :status] 200))) +``` + +and use it: + +```clojure +(defn logout + [state] + (let [session-store (get-in state [:deps :session-backend]) + session-id (get-in state [:session-data :session-id])] + (session/delete! session-store session-id) + (-> state + (dissoc :session-data) + (assoc :view views/logout-view)))) ``` diff --git a/doc/interceptors.md b/doc/interceptors.md index 38e222a9..48015d54 100644 --- a/doc/interceptors.md +++ b/doc/interceptors.md @@ -1,3 +1,5 @@ + + # Interceptors implemented in Xiana - [log](#log) diff --git a/doc/modules.md b/doc/modules.md index 864f4b2b..9f8c422a 100644 --- a/doc/modules.md +++ b/doc/modules.md @@ -1,59 +1,54 @@ + + # Modules -- [Backend](#backend) - - [Auth](#auth) - - [Config](#config) - - [Database](#database) - - [Database/main](#databasemain) - - [Database/seed](#databaseseed) - - [Database/core](#databasecore) - - [Migrations](#migrations) - - [Interceptor](#interceptor) - - [Interceptor/core](#interceptorcore) - - [Interceptor/muuntaja](#interceptormuuntaja) - - [Interceptor/wrap](#interceptorwrap) - - [Mail](#mail) - - [RBAC](#rbac-1) - - [Route](#route) - - [Scheduler](#scheduler) - - [SSE](#sse) - - [Session](#session) - - [State](#state-1) - - [Webserver](#webserver) - -## Backend - -### Auth +- [Auth](#auth) +- [Config](#config) +- [Database](#database) + - [Database/main](#databasemain) + - [Database/core](#databasecore) +- [Migrations](#migrations) +- [Interceptor](#interceptor) + - [Interceptor/core](#interceptorcore) + - [Interceptor/muuntaja](#interceptormuuntaja) + - [Interceptor/wrap](#interceptorwrap) +- [Mail](#mail) +- [RBAC](#rbac-1) +- [Route](#route) +- [Scheduler](#scheduler) +- [SSE](#sse) +- [Session](#session) +- [State](#state) +- [Webserver](#webserver) + + + +## Auth Auth.core is a package to deal with password encoding, and validation. -### Config +## Config config/core provides functions to deal with environment variables and config.edn files. -### Database +## Database Database handling functions. -#### Database/main +## Database/main Migratus wrapper to get rid of lein migratus plugin. As well as support profile dependent configuration of migratus. -#### Database/seed - -Migratus wrapper to support profile based separation of data seeding - - -#### Database/core +## Database/core Start function, which gets the data source based on given configuration. Query executor functions and the db-access interceptor. -### Interceptor +## Interceptor Some default interceptors and helpers. -#### Interceptor/core +## Interceptor/core Collection of interceptors, like: @@ -66,49 +61,50 @@ Collection of interceptors, like: - session-user-role - muuntaja -#### Interceptor/muuntaja +## Interceptor/muuntaja Default settings, and format instance of content negotiation. -#### Interceptor/queue +## Interceptor/queue The queue is the interceptor executor. It contains all functions necessary to the handler to deal with interceptors and interceptor overrides. -#### Interceptor/wrap +## Interceptor/wrap With interceptor wrapper you can use any kind of interceptors and middlewares with xiana provided flow. -### Mail +## Mail Helps you to send an email message based on configuration. -### RBAC +## RBAC Wrapper package for tiny-RBAC lib. It initializes role-set and gathers permissions from the actual state. It contains an interceptor too, to deal with permissions and restrictions. -### Route +## Route Contains all functions to deal with route dependent functionality. Uses reitit matcher and router. Collects request-data for processing it via controller interceptors and action. -### Scheduler +## Scheduler Repeated function execution. The function gets `:deps` as parameter. -### SSE +## SSE Server-sent events implementation based on HTTP-kit's Channel protocol. -### Session +## Session -Contains Session protocol definition, an in-memory, and persistent (postgres) session backend, and the session interceptor. +Contains Session protocol definition, an in-memory, and persistent (postgres) session backend, and the session +interceptor. -### State +## State In handler-fn it creates the initial state for request execution. -### Webserver +## Webserver Starts a jetty server with the default handler, provides the dependencies to the handler function. diff --git a/resources/images/.gitkeep b/doc/resources/images/.gitkeep similarity index 100% rename from resources/images/.gitkeep rename to doc/resources/images/.gitkeep diff --git a/resources/images/Xiana.png b/doc/resources/images/Xiana.png similarity index 100% rename from resources/images/Xiana.png rename to doc/resources/images/Xiana.png diff --git a/resources/images/around-and-inside.png b/doc/resources/images/around-and-inside.png similarity index 100% rename from resources/images/around-and-inside.png rename to doc/resources/images/around-and-inside.png diff --git a/resources/images/around.png b/doc/resources/images/around.png similarity index 100% rename from resources/images/around.png rename to doc/resources/images/around.png diff --git a/resources/images/flow.png b/doc/resources/images/flow.png similarity index 100% rename from resources/images/flow.png rename to doc/resources/images/flow.png diff --git a/resources/images/inside.png b/doc/resources/images/inside.png similarity index 100% rename from resources/images/inside.png rename to doc/resources/images/inside.png diff --git a/resources/images/override.png b/doc/resources/images/override.png similarity index 100% rename from resources/images/override.png rename to doc/resources/images/override.png diff --git a/resources/images/success.png b/doc/resources/images/success.png similarity index 100% rename from resources/images/success.png rename to doc/resources/images/success.png diff --git a/doc/tutorials.md b/doc/tutorials.md index 99881d86..3ea7c957 100644 --- a/doc/tutorials.md +++ b/doc/tutorials.md @@ -1,64 +1,90 @@ + + # Tutorials -- [Dependencies and configuration](#dependencies-and-configuration) -- [Database migration](#database-migration) -- [Database seed with data](#database-seed-with-data) -- [Interceptors typical use-case, and ordering](#interceptors-typical-use-case-and-ordering) -- [Defining new interceptors](#defining-new-interceptors) - - [Interceptor example](#interceptor-example) +- [Application startup](#application-startup) +- [Configuration](#configuration) +- [Dependencies](#dependencies) - [Router and controller interceptors](#router-and-controller-interceptors) -- [Providing default interceptors](#providing-default-interceptors) -- [Interceptor overriding](#interceptor-overriding) - [Routes](#routes) - [Action](#action) +- [Database migration](#database-migration) - [Database-access](#database-access) - [View](#view) - [Side-effects](#side-effects) - [Session management](#session-management) -- [Role based access and data ownership control](#role-based-access-and-data-ownership-control) + - [In memory backend](#in-memory-session-backend) + - [Persistent backend](#persistent-session-backend) + - [Session interceptors](#session-interceptors) - [WebSockets](#websockets) - [WebSockets routing](#websockets-routing) - [Route matching](#route-matching) - [Server-Sent Events (SSE)](#server-sent-events-sse) - [Scheduler](#scheduler) -## Dependencies and configuration +## Application startup + +Starting up an application takes several well-defined steps: + +- reading the configuration +- setting up dependencies +- spinning up a web-server + +## Configuration + +Apps built with Xiana are configurable in several ways. It uses [yogthos/config](https://github.com/yogthos/config) to +resolve basic configuration from `config.edn`, `.lein-env`, `.boot-env` files, environment variables and system +properties. Additionally + +- Xiana looks for an `.edn` file pointed with `:xiana-config` variable for overrides +- You can define a key to read from any other (already defined) value, and/or pass a default value. + +In practice this means you can define a config value like this: + +```clojure +:xiana/test {:test-value-1 "$property" + :test-value-2 "$foo | baz"} +``` + +and this will be resolved as + +```clojure +:xiana/test {:test-value-1 "the value of 'property' key, or nil" + :test-value-2 "the value of `foo` key or \"baz\""} +``` + +The value of property key can come from the config files, environment variables, or from system properties. -Almost all components that you need on runtime should be reachable via the passed around state. To achieve this it -should be part of the :deps map in the state. Any other configuration what you need in runtime should be part of this -map too. +## Dependencies + +Database connection, external APIs or session storage, the route definition, setting up scheduled executors or +doing migrations are our dependencies. These dependencies should be reachable via the passed around state. To achieve +this, it should be part of the `:deps` map in the state. Any other configuration what you need in runtime should be part +of this map too. The system configuration and start-up with the chainable set-up: ```clojure (defn ->system [app-cfg] - (-> (config/config) - (merge app-cfg) - (rename-key :xiana/auth :auth) - (rename-key :xiana/uploads :uploads) - routes/reset - session/init-backend - sse/init - db/start - db/migrate! - (scheduler/start actions/ping 10000) - (scheduler/start actions/execute-scheduled-actions (* 60 1000)) - ws/start - closeable-map)) - -(defn app-cfg - [config] - {:routes routes - :router-interceptors [(spa-index/wrap-default-spa-index "/re-frame")] - :controller-interceptors (concat [(xiana-interceptors/muuntaja) - cookies/interceptor - xiana-interceptors/params - (session/protected-interceptor "/api" "/login") - xiana-interceptors/view - xiana-interceptors/side-effect - db/db-access] - (:controller-interceptors config))}) + (-> (config/config app-cfg) ;Read config + routes/reset ;set up routing + db/start ;set up database connection + db/migrate! ;running migrations + session/init-backend ;initialize session storage + (scheduler/start actions/ping 10000) ;starting a scheduler + ws/start)) ;spinning up the webserver + +(def app-cfg + {:routes routes ;injecting route definition + :router-interceptors [] ;definition of router interceptors + :controller-interceptors [(xiana-interceptors/muuntaja) ;definition of controller interceptors + cookies/interceptor + xiana-interceptors/params + session/interceptor + xiana-interceptors/view + xiana-interceptors/side-effect + db/db-access]}) (defn -main [& _args] @@ -179,17 +205,17 @@ The provided function should have one parameter, the application state, and shou (-> state (transform-somehow) (or-do-side-effects)) - :leave (fn [state] - (println "Leave: " state) - state) - :error (fn [state] - (println "Error: " state) - ;; Here `state` should have previously thrown exception - ;; stored in `:error` key. - ;; you can do something useful with it (e.g. log it) - ;; and/or handle it by `dissoc`ing from the state. - ;; In that case remaining `leave` interceptors will be executed. - (assoc state :response {:status 500 :body "Error occurred while printing out state"}))} + :leave (fn [state] + (println "Leave: " state) + state) + :error (fn [state] + (println "Error: " state) + ;; Here `state` should have previously thrown exception + ;; stored in `:error` key. + ;; you can do something useful with it (e.g. log it) + ;; and/or handle it by `dissoc`ing from the state. + ;; In that case remaining `leave` interceptors will be executed. + (assoc state :response {:status 500 :body "Error occurred while printing out state"}))} ``` #### Router and controller interceptors @@ -207,135 +233,167 @@ The handler function executes interceptors in this order 6. controller interceptors :leave functions in reversed order In router interceptors, you are able to interfere with the routing mechanism. Controller interceptors can be interfered -with via route definition. - -## Providing default interceptors +with via route definition. There is an option to define interceptors around creating WebSocket channels, these +interceptors are executed around the `:ws-action` instead of `:action`. -The router and controller interceptors definition is part of the application startup. The system's dependency map should -contain two sequence of interceptors like +## Routes -```clojure -{:router-interceptors [...] - :controller-interceptors [...]} -``` +Route definition is done via [reitit's routing](https://github.com/metosin/reitit) library. Route processing is done +with `xiana.route` namespace. At route definition you can define. -## Interceptor overriding +- The [action](#action) that should be executed +- [Interceptor overriding](how-to.md#interceptor-overriding) +- The required permission for [rbac](how-to.md#role-based-access-and-data-ownership-control) +- [WebSockets](#websockets) action definition -On route definition you can interfere with the default controller interceptors. With the route definition you are able -to set up different controller interceptors other than the ones already defined with the app. There are three ways to do -it: +If any extra parameter is provided here, it's injected into ```clojure -... {:action #(do something) - :interceptors [...]} +(-> state :request-data :match) ``` -will override all controller interceptors - -```clojure -... {:action #(do something) - :interceptors {:around [...]}} -``` +in routing step. -will extend the defaults around +Example route definition: ```clojure -... {:action #(do something) - :interceptors {:inside [...]}} +["/api" {} + ["/login" {:post {:action #'user-controllers/login ;Login controller + :interceptors {:except [session/interceptor]}}}] ;the user doesn't have a valid session yet + ["/posts" {:get {:action #'posts-controllers/fetch ;controller definition for fetching posts + :permission :posts/read} ;set up the permission for fetching posts + :put {:action #'posts-controllers/add + :permission :posts/create} + :post {:action #'posts-controllers/update-post + :permission :posts/update} + :delete {:action #'posts-controllers/delete-post + :permission :posts/delete}}] + ["/notifications" {:get {:ws-action #'websockets/notifications ;websocket controller for sending notifications + :action #'notifications/fetch ;REST endpoint for fetching notifications + :permission :notifications/read}}] ;inject permission + ["/style" {:get {:action #'style/fetch + :organization :who}}]] ;this is not a usual key, the value will go to +;(-> state :request-data :match :organization) +;at the routing process ``` -will extend the defaults inside +## Action + +The action function (controller) in a +single [CRUD application](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete#RESTful_APIs) is for defining +a [view](#view), a [database-query](#database-access) (model) and optionally a [side-effect](#side-effects) function +which will be executed in the following interceptor steps. ```clojure -... {:action #(do something) - :interceptors {:inside [...] - :around [...]}} +(defn action + [state] + (assoc state :view view/success + :side-effect behaviour/update-sessions-and-db! + :query model/fetch-query)) ``` -will extend the defaults inside and around +## Database migration -```clojure -... {:action #(do something) - :interceptors {:except [...]}} -``` +Database migration is based on the following principles: -will skip the excepted interceptors from defaults +1. The migration process is based on a stack of immutable changes. If at some point you want to change the schema or the + content of the database you don't change the previous scripts but add new scripts at the top of the stack. +2. There should be a single standard resources/migrations migration directory +3. If a specific platform (dev, stage, test, etc) needs additional scripts, specific directories should be created and + in config set the appropriate migrations-dir as a vector containing the standard directory and the auxiliary + directory. +4. The order in which scripts are executed depends only on the script id and not on the directory where the script is + located -The execution flow will look like this +### Configure migration -1. router interceptors :enters in order -2. router interceptors :leaves in reversed order -3. routing -4. around interceptors :enters in order -5. controller interceptors :enters in order -6. inside interceptors :enters in order -7. action -8. inside interceptors :leaves in reversed order -9. controller interceptors :leaves in reversed order -10. around interceptors :leaves in reversed order +The migration process requires a config file containing: -All interceptors in :except will be skipped. +```clojure +:xiana/postgresql {:port 5432 + :dbname "framework" + :host "localhost" + :dbtype "postgresql" + :user "postgres" + :password "postgres"} +:xiana/migration {:store :database + :migration-dir ["resources/migrations"] + :init-in-transaction? false + :migration-table-name "migrations"} +``` -## Routes +The :migration-dir param is a vector of classpath relative paths containing database migrations scripts. -Route definition is done via [reitit's routing](https://github.com/metosin/reitit) library. Route processing is done -with `xiana.route` namespace. At route definition you can define. +### Usage -- The [action](#action) that should be executed -- [Interceptor overriding](#interceptor-overriding) -- The required permission for [rbac](#role-based-access-and-data-ownership-control) -- [WebSocket](#websocket) action definition +The `xiana.db.migrate` implements a cli for migrations framework. -If any extra parameter is provided here, it's injected into +If you add to `deps.edn` in `:aliases` section: ```clojure -(-> state :request-data :match) +:migrate {:main-opts ["-m" "xiana.db.migrate"]} ``` -in routing step. +you could access this cli from clojure command. -## Action +To see all commands and options available run: + +```shell +clojure -M:migrate --help +``` -The action function in a single -[CRUD application](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete#RESTful_APIs) is for defining a -[view](#view), a [database-query](#database-access) and optionally a [side-effect](#side-effects) function which will be -executed in the following interceptor steps. +Examples of commands: -```clojure -(defn action - [state] - (assoc state :view view/success - :side-effect behaviour/update-sessions-and-db! - :query model/fetch-query)) +```shell +# update the database to current version: +clojure -M:migrate migrate -c resources/config.edn +# rollback the last run migration script: +clojure -M:migrate rollback -c resources/config.edn +# rollback the database down until id script: +clojure -M:migrate rollback -i 20220103163538 -c resources/config.edn +# create the migrations scripts pair: +clojure -M:migrate create -d resources/migrations -n the-name-of-the-script ``` -## Database-access +## Database access + +The `xiana.db/db-access` executes queries from `:query` (for single, non-transactional database access) +and `:db-queries` in this order against the datasource extracted from state. The result will be available +in `(-> state :response-data :db-data)` which is always a sequence. -The `database.core`'s interceptor extracts the datasource from the provided state parameter and the :query. +`db-queries` is still a map, contains `:queries` and `:transaction?` keys. If `:transaction?` is set to `true`, +all `queries` in `db-queries` will be executed in one transaction. The query should be in [honey SQL](https://github.com/nilenso/honeysql-postgres) format, it will be sql-formatted on execution: ```clojure -(defn fetch-query - [state] - (let [login (-> state :request :body-params :login)] - (-> (select :*) - (from :users) - (where [:and - :is_active - [:or - [:= :email login] - [:= :username login]]])))) +(-> (select :*) + (from :users) + (where [:and + :is_active + [:or + [:= :email login] + [:= :username login]]])) ``` -The execution always has `{:return-keys true}` parameter and the result goes into +is equal to ```clojure -(-> state :response-data :db-data) +{:select [:*] + :from [:users] + :where [:and + :is-active + [:or + [:= :email login] + [:= :user-name login]]]} ``` -without any transformation. +Both examples above are leads to + +```postgres-sql +["SELECT * FROM users WHERE is_active AND (email = ? OR user_name = ?)" login login] +``` ## View @@ -345,21 +403,22 @@ A view is a function to prepare the final response and saving it into the state (defn success [state] (let [{:users/keys [id]} (-> state :response-data :db-data first)] - (assoc state :response {:status 200 - :headers {"Content-type" "Application/json"} - :body {:view-type "login" - :data {:login "succeed" - :user-id id}}}))) + (assoc state :response {:status 200 + :body {:view-type "login" + :data {:login "succeed" + :user-id id}}}))) ``` ## Side-effects -Conventionally, side-effects interceptor is placed after [action](#action) and [database-access](#database-access), just +Conventionally, side-effects interceptor is placed after [action](#action) and [database-access](#database-access), +just right before [view](#view). At this point, we already have the result of database execution, so we are able to do some extra refinements, like sending notifications, updating the application state, filtering or mapping the result and so on. -Adding to the previous examples: +This example shows you, how can you react on a login request. This stores the user data in the actual session on +successful login, or injects the `Unauthorized` response into the state. ```clojure (defn update-sessions-and-db! @@ -370,10 +429,8 @@ Adding to the previous examples: (if (valid-credentials? state) (let [new-session-id (str (UUID/randomUUID)) session-backend (-> state :deps :session-backend) - {:users/keys [id] :as user} (-> state :response-data :db-data first)] - (remove-from-session-store! session-backend id) + user (-> state :response-data :db-data first)] (xiana-sessions/add! session-backend new-session-id user) - (update-user-last-login! state id) (assoc-in state [:response :headers "Session-id"] new-session-id)) (throw (ex-info "Missing session data" {:xiana/response @@ -383,70 +440,99 @@ Adding to the previous examples: ## Session management -Session interceptor interchanges session data between the session-backend and the app state. +Session management has two mayor components -On `:enter` it loads the session by its session-id, into `(-> state :session-data)` +- session backend +- session interceptors -The session-id can be provided either in headers, cookies, or as query-param. When session-id is found nowhere or is an -invalid UUID, or the session is not stored in the storage, then the response will be: +The session backend can be in-memory or persisted using a json storage in postgres database. -```clojure -{:status 401 - :body "Invalid or missing session"} -``` +### In memory session backend -On the `:leave` branch, updates session storage with the data from `(-> state :session-data)` +Basically it's an atom backed session protocol implementation, allows you to `fetch` `add!` `delete!` `dump` +and `erase!` session data or the whole session storage. It doesn't require any additional configuration, and this is +the default set up for handling session storage. All stored session data is wiped out on system restart. -## Role based access and data ownership control +### Persistent session backend -To get the benefits of [tiny RBAC](https://github.com/Flexiana/tiny-rbac) library you need to provide the resource and -the action for your endpoint in [router](#routes) definition: +Instead of atom, it uses a postgresql table to store session data. Has the same protocol as in-memory. Configuration is +necessary to use it. -```clojure -[["/api" - ["/image" {:delete {:action delete-action - :permission :image/delete}}]]] +- it's necessary to have a table in postgres: + +```postgres-sql +CREATE TABLE sessions ( + session_data json not null, + session_id uuid primary key, + modified_at timestamp DEFAULT CURRENT_TIMESTAMP +); ``` -and add your role-set into your app's [dependencies](#dependencies-and-configuration): +- you need to define the session's configuration in you `config.edn` files: ```clojure -(defn ->system - [app-cfg] - (-> (config/config) - (merge app-cfg) - xiana.rbac/init - ws/start)) + :xiana/session-backend {:storage :database + :session-table-name :sessions} ``` -On `:enter`, the interceptor performs the permission check. It determines if the action allowed for the user found -in `(-> state :session-data :user)`. If access to the resource/action isn't permitted, then the response is: +- in case of + - missing `:storage` key, `in-memory` session backend will be used + - missing `:session-table-name` key, `:sessions` table will be used -```clojure -{:status 403 - :body "Forbidden"} -``` +- the database connection can be configured in three ways: + + - via additional configuration -If a permission is found, then it goes into `(-> state :request-data :user-permissions)` as a parameter for data -ownership processing. + ```clojure + :xiana/session-backend {:storage :database + :session-table-name :sessions + :port 5433 + :dbname "app-db" + :host "localhost" + :dbtype "postgresql" + :user "db-user" + :password "db-password"} + ``` -On `:leave`, executes the restriction function found in `(-> state :request-data :restriction-fn)`. The `restriction-fn` -should look like this: + - using the same datasource as the application use: + + Just init the backend after the database connection + ```clojure + (defn ->system + [app-cfg] + (-> (config/config app-cfg) + routes/reset + db-core/connect + db-core/migrate! + session/init-backend + ws/start)) + ``` + + - Creating new datasource + + If no datasource is provided on initialization, the `init-backend` function merges the database config with the + session backend configuration, and creates a new datasource from the result. + +### Session interceptors + +The session interceptors interchanges session data between the session-backend and the app state. + +The `xiana.session/interceptor` throws an exception when no valid session-id can be found in the headers, cookies or as +query parameter. + +The `xiana.session/guest-session-interceptor` creates a guest session if the session-id is missing, or invalid, which +means: ```clojure -(defn restriction-fn - [state] - (let [user-permissions (get-in state [:request-data :user-permissions])] - (cond - (user-permissions :image/all) state - (user-permissions :image/own) (let [session-id (get-in state [:request :headers "session-id"]) - session-backend (-> state :deps :session-backend) - user-id (:users/id (session/fetch session-backend session-id))] - (update state :query sql/merge-where [:= :owner.id user-id]))))) +{:session-id (UUID/randomUUID) + :users/role :guest + :users/id (UUID/randomUUID)} ``` -The rbac interceptor must be placed between the [action](#action) and the [db-access](#database-access) interceptors in -the [interceptor chain](#interceptors-typical-use-case-and-ordering). +will be injected to the session data. + +Both interceptors fetching already stored session data into the state at `:enter`, and on `:leave` updates session +storage with the data from `(-> state :session-data)` ## WebSockets @@ -499,7 +585,7 @@ have the entire [state](conventions.md#state) to work with. `xiana.websockets` offers a router function, which supports Xiana concepts. You can define a reitit route and use it inside WebSockets reactive functions. With Xiana [state](conventions.md#state) -and support of [interceptors](conventions.md#interceptors), with [interceptor override](#interceptor-overriding). You +and support of [interceptors](conventions.md#interceptors), with [interceptor override](how-to.md#interceptor-overriding). You can define a [fallback function](#websockets), to handle missing actions. ```clojure @@ -529,26 +615,18 @@ For route matching Xiana provides a couple of modes: - Probe - It tries to decode the message as JSON, then as EDN, then as string. + It tries to decode the message as JSON, EDN or string in corresponding order. You can also define your own matching, and use it as a parameter to `xiana.websockets/router` ## Server-Sent Events (SSE) -Xiana contains a simple SSE solution over [http-kit](https://github.com/http-kit/http-kit) server's `Channel` -protocol. +Xiana contains a simple SSE solution over WebSockets protocol. -Initialization is done by calling `xiana.sse/init`. Clients can subscribe by routing to `xiana.sse/sse-action`. Messages -are sent with `xiana.sse/put!` function. +Initialization is done by calling `xiana.sse/init`. Clients can subscribe by a route +with `xiana.sse/sse-action` as `:ws-action`. Messages are sent with `xiana.sse/put!` function. ```clojure -(ns app.core - (:require - [xiana.config :as config] - [xiana.sse :as sse] - [xiana.route :as route] - [xiana.webserver :as ws])) - (def routes [["/sse" {:action sse/sse-action}] ["/broadcast" {:action (fn [state] @@ -557,17 +635,14 @@ are sent with `xiana.sse/put!` function. (defn ->system [app-cfg] - (-> (config/config) - (merge app-cfg) + (-> (config/config app-cfg) + (route/reset routes) sse/init ws/start)) -(def app-cfg - {:routes routes}) - (defn -main [& _args] - (->system app-cfg)) + (->system {})) ``` ## Scheduler diff --git a/doc/welcome.md b/doc/welcome.md new file mode 100644 index 00000000..12a283a4 --- /dev/null +++ b/doc/welcome.md @@ -0,0 +1,55 @@ + + +# Xiana framework + +Xiana is a lightweight web-application framework written in Clojure, for Clojure. The goal is to be simple, fast, and +most importantly - a welcoming platform for web programmers with different backgrounds who want to experience the +wonders +of functional programming! + +It's easy to install, fun to experiment with, and a powerful tool to produce monolithic web applications. + +## Installation + +### From template + +Xiana has its own Leiningen template, so you can create a skeleton project with + +```shell +lein new xiana app +``` + +### As a dependency + +Add it to your project as a dependency from clojars: + +[![Clojars Project](https://img.shields.io/clojars/v/com.flexiana/framework.svg)](https://clojars.org/com.flexiana/framework) + +## Getting started + +This [document](./getting-started.md) explains how to use Xiana to create a very simple app with a db, a backend +offering an API, and a frontend that displays something from the database. + +## Docs + +- First check out the [conventions](./conventions.md). +- To start working with xiana, read the [tutorials](./tutorials.md). +- To contribute, see the [contribution](./contribution.md) docs. + +### Examples + +Visit [examples folder](https://github.com/Flexiana/framework/tree/main/examples), to see how you can perform + +- Access and data ownership control +- Request coercion and response validation +- Session handling with varying interceptors +- Chat platform with WebSockets +- Event based resource handling + +## References + +### Concept of interceptors + +[Pedestal](http://pedestal.io/reference/interceptors) + +[Sieppari](https://github.com/metosin/sieppari) diff --git a/docs/0.5.0-rc2/contribution.html b/docs/0.5.0-rc2/contribution.html new file mode 100644 index 00000000..6578272d --- /dev/null +++ b/docs/0.5.0-rc2/contribution.html @@ -0,0 +1,108 @@ + +Contribution

+

Contribution

+ +

Ticket system

+

We’re using GitHub issues for tracking and discussing ideas and requests.

+

Coding standards

+

Please follow clj-style and kondo instructions. Kibit isn’t a showstopper, but PRs are more welcomed if not breaking kibit.

+

Submitting a PR

+

Before you are submitting a PR be sure:

+
    +
  • You’ve updated the documentation and the CHANGELOG
  • +
  • the PR has an issue in GitHub, with a good description
  • +
  • you have added tests
  • +
  • you provided an example project for a new feature
  • +
  • All PRs need at least two approvals
  • +
  • Follow Semantic Versioning 2.0.0
  • +
+

Development dependencies

+

Mandatory

+
    +
  • Clojure 1.10
  • +
  • Postgresql >= 11.5
  • +
  • Clojure cli >= 1.11.1.1155
  • +
  • Docker >= 19.03.11
  • +
  • Docker-compose >= 1.21.0
  • +
+

Libraries

+

Mandatory

+ + + + + + + + + + + + + + + + + + +
Name Version Related
funcool/cuerdas RELEASE String manipulation
metosin/reitit 0.5.12 Routes
potemkin/potemkin 0.4.5 Helper
com.draines/postal 2.0.4 Email
duct/server.http.jetty 0.2.1 WebServer
seancorfield/next.jdbc 1.1.613 WebServer
honeysql/honeysql 1.0.444 PostGreSQL
nilenso/honeysql-postgres 0.2.6 PostGreSQL
org.postgresql/postgresql 42.2.2 PostGreSQL
crypto-password/crypto-password 0.2.1 Security
clj-kondo/clj-kondo RELEASE Tests
npx RELEASE Documentation
+

Setup

+
$ git clone git@github.com:Flexiana/framework.git; cd framework
+$ ./script/auto.sh -y all
+
+

The first command will clone Flexiana/framework repository and jump to its directory. The second command calls auto.sh script to perform the following sequence of steps:

+
    +
  1. Download the necessary docker images
  2. +
  3. Instantiate the database container
  4. +
  5. Import the initial SQL schema: ./docker/sql-scripts/init.sql
  6. +
  7. Populate the new schema with ‘fake’ data from: ./docker/sql-scripts/test.sql
  8. +
  9. Call clj -X:test that will download the necessary Clojure dependencies and executes unitary tests.
  10. +
+

See ./script/auto.sh help for more advanced options.

+

Remember it’s necessary to have docker & docker-compose installed in your host machine. Docker daemon should be running. The chain of commands fails otherwise. It should also be noted that after the first installation everything will be cached preventing unnecessary rework, it’s possible to run only the tests, if your development environment is already up, increasing the overall productivity.

+
./script/auto.sh -y tests
+
+

Releasing

+

Install locally

+
clj -M:install
+
+

Deploying a release

+
    +
  • Set up a new version number in release.edn eg: "0.5.0-rc2"
  • +
  • Make a git TAG with v prefix, like v0.5.0-rc2
  • +
  • Push it and wait for deployment to clojars
  • +
+

Executing example’s tests

+
    +
  • Be sure all examples has the same framework version as it is in release.edn as dependency
  • +
  • Execute ./example-tests.sh script. It will install the actual version of xiana, and go through the examples folder for check-style and lein test.
  • +
+

Generating API Docs

+

This is done with using mermaid-cli and a forked version of Codox.

+

We’re using mermaid-cli for render UML-diagrams in markdown files, see the doc/conventions_template.md for example. These files need to be added to the /script/build-docs.sh . For using it you need to have npx.

+

Codox is forked because markdown anchors aren’t converted to HTML anchors in the official release. For use, you need to

+
git clone git@github.com:Flexiana/codox.git
+cd codox/codox
+lein install
+
+

it before generating the documentation.

+

To generate or update the current version run the script:

+
./script/build-docs.sh
+
+

This runs the following:

+
npx -py @mermaid-js/mermaid-cli mmdc -i doc/conventions_template.md -o doc/conventions.md
+clj -X:codox
+mv docs/new docs/{{version-number}}
+
+

It also updates the index.html file to point to the new version.

+
\ No newline at end of file diff --git a/docs/0.5.0-rc2/conventions-1.svg b/docs/0.5.0-rc2/conventions-1.svg new file mode 100644 index 00000000..ee25487a --- /dev/null +++ b/docs/0.5.0-rc2/conventions-1.svg @@ -0,0 +1 @@ +Web serverHandlerRouter interceptorsRouterController interceptorsActionEnhancing app state with requestEnter functionsMatching, routingLeave functions in reverted orderEnter functionsExecuting controllerLeave functions in reverted orderExtracting responseRequestStateStateStateStateStateStateStateResponseWeb serverHandlerRouter interceptorsRouterController interceptorsAction \ No newline at end of file diff --git a/docs/0.5.0-rc2/conventions-2.svg b/docs/0.5.0-rc2/conventions-2.svg new file mode 100644 index 00000000..9b2d93b6 --- /dev/null +++ b/docs/0.5.0-rc2/conventions-2.svg @@ -0,0 +1 @@ +HandlerInterceptor_IInterceptor_IIInterceptor_IIIAction:enter:enter:enterDo something:leave:leave:leaveStateStateStateStateStateStateStateStateHandlerInterceptor_IInterceptor_IIInterceptor_IIIAction \ No newline at end of file diff --git a/docs/0.5.0-rc2/conventions-3.svg b/docs/0.5.0-rc2/conventions-3.svg new file mode 100644 index 00000000..20ac2909 --- /dev/null +++ b/docs/0.5.0-rc2/conventions-3.svg @@ -0,0 +1 @@ +HandlerInterceptor_IInterceptor_IIInterceptor_IIIAction:enter:enter:enter - Throwns an exception:error - Handles exception:leave:leave:error - Doesn't handles exception:error - Handles exception:error - Doesn't handles exception:error - Handles exception:leavealt[Interceptor_I handles the exception][Interceptor_II handles the exception]alt[Exception handled by thower][Exception not handled by thrower]Extract responseStateStateStateStateStateStateStateStateStateStateStateHandlerInterceptor_IInterceptor_IIInterceptor_IIIAction \ No newline at end of file diff --git a/docs/0.5.0-rc2/conventions.html b/docs/0.5.0-rc2/conventions.html new file mode 100644 index 00000000..f86b761d --- /dev/null +++ b/docs/0.5.0-rc2/conventions.html @@ -0,0 +1,60 @@ + +Conventions

+

Conventions

+ +

Overview

+

The diagram bellow gives you an overview, how a request is processed in Xiana based applications.

+

diagram

+

State

+

State is created for each HTTP request and represents the current state of the application. It contains:

+
    +
  • the application’s dependencies and configuration
  • +
  • request
  • +
  • request-data
  • +
  • response
  • +
+

This structure is very volatile, will be updated quite often on the application’s life cycle.

+

The main modules that update the state are:

+
    +
  • Routes:
  • +
+

Add information from the matched route to the state map

+
    +
  • Interceptors:
  • +
+

Add, consumes or remove information from the state map. More details in Interceptors section.

+
    +
  • Actions:
  • +
+

In actions, you are able to interfere with the :leave parts of the interceptors.

+

At the last step of execution the handler extracts the response value from the state.

+

The state is renewed on every request.

+

Action

+

The action conventionally is the control point of the application flow. This is the place were you can define how the rest of your execution flow would behave. Here you can provide the database query, restriction function, the view, and the additional side effect functions are you want to execute.

+

Actions are defined in the routes vector

+
["/" {:get {:action #(do something)}}]
+
+

Handler

+

Xiana’s handler creates the state for every request, matches the appropriate route, executes the interceptors, handles interceptor overrides, and not-found cases. It handles websocket requests too.

+

Routing

+

Routing means selecting the actions to execute depending on the request URL, and HTTP method.

+

Dependencies

+

Modules can depend on external resources, configurations, as well as on other modules. These dependencies are added to the state on state creation, and defined on application startup.

+

Interceptors

+

An interceptor is a pair of unary functions. Each function must recieve and return a state map. You can look at it as on an analogy to AOP’s around aspect, or as on a pair of middlewares. They work mostly the same way as pedestal and sieppari interceptors. Xiana provides a set of base interceptors, for the most common use cases.

+

This figure shows how interceptors are executed ideally:

+

diagram

+

Interceptors error handling:

+

The interceptor executor handles the exceptional states like sieppari does. If an exception happens, it tries to handle first in the same interceptor. If it has and :error handler, it will call it, otherwise it’ll search for :error handlers for the beginning of the interceptor queue. When and :error function found, and matched with the given exception, the executor calls the queue :leave functions in reserved order, where the handler has been found.

+

This diagram shows how the error cases handled:

+

diagram

+
\ No newline at end of file diff --git a/docs/0.5.0-rc2/css/default.css b/docs/0.5.0-rc2/css/default.css new file mode 100644 index 00000000..33f78fed --- /dev/null +++ b/docs/0.5.0-rc2/css/default.css @@ -0,0 +1,551 @@ +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 15px; +} + +pre, code { + font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; + font-size: 9pt; + margin: 15px 0; +} + +h1 { + font-weight: normal; + font-size: 29px; + margin: 10px 0 2px 0; + padding: 0; +} + +h2 { + font-weight: normal; + font-size: 25px; +} + +h5.license { + margin: 9px 0 22px 0; + color: #555; + font-weight: normal; + font-size: 12px; + font-style: italic; +} + +.document h1, .namespace-index h1 { + font-size: 32px; + margin-top: 12px; +} + +#header, #content, .sidebar { + position: fixed; +} + +#header { + top: 0; + left: 0; + right: 0; + height: 22px; + color: #f5f5f5; + padding: 5px 7px; +} + +#content { + top: 32px; + right: 0; + bottom: 0; + overflow: auto; + background: #fff; + color: #333; + padding: 0 18px; +} + +.sidebar { + position: fixed; + top: 32px; + bottom: 0; + overflow: auto; +} + +.sidebar.primary { + background: #e2e2e2; + border-right: solid 1px #cccccc; + left: 0; + width: 250px; +} + +.sidebar.secondary { + background: #f2f2f2; + border-right: solid 1px #d7d7d7; + left: 251px; + width: 200px; +} + +#content.namespace-index, #content.document { + left: 251px; +} + +#content.namespace-docs { + left: 452px; +} + +#content.document { + padding-bottom: 10%; +} + +#header { + background: #3f3f3f; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); + z-index: 100; +} + +#header h1 { + margin: 0; + padding: 0; + font-size: 18px; + font-weight: lighter; + text-shadow: -1px -1px 0px #333; +} + +#header h1 .project-version { + font-weight: normal; +} + +.project-version { + padding-left: 0.15em; +} + +#header a, .sidebar a { + display: block; + text-decoration: none; +} + +#header a { + color: #f5f5f5; +} + +.sidebar a { + color: #333; +} + +#header h2 { + float: right; + font-size: 9pt; + font-weight: normal; + margin: 4px 3px; + padding: 0; + color: #bbb; +} + +#header h2 a { + display: inline; +} + +.sidebar h3 { + margin: 0; + padding: 10px 13px 0 13px; + font-size: 19px; + font-weight: lighter; +} + +.sidebar h3 a { + color: #444; +} + +.sidebar h3.no-link { + color: #636363; +} + +.sidebar ul { + padding: 7px 0 6px 0; + margin: 0; +} + +.sidebar ul.index-link { + padding-bottom: 4px; +} + +.sidebar li { + display: block; + vertical-align: middle; +} + +.sidebar li a, .sidebar li .no-link { + border-left: 3px solid transparent; + padding: 0 10px; + white-space: nowrap; +} + +.sidebar li .no-link { + display: block; + color: #777; + font-style: italic; +} + +.sidebar li .inner { + display: inline-block; + padding-top: 7px; + height: 24px; +} + +.sidebar li a, .sidebar li .tree { + height: 31px; +} + +.depth-1 .inner { padding-left: 2px; } +.depth-2 .inner { padding-left: 6px; } +.depth-3 .inner { padding-left: 20px; } +.depth-4 .inner { padding-left: 34px; } +.depth-5 .inner { padding-left: 48px; } +.depth-6 .inner { padding-left: 62px; } + +.sidebar li .tree { + display: block; + float: left; + position: relative; + top: -10px; + margin: 0 4px 0 0; + padding: 0; +} + +.sidebar li.depth-1 .tree { + display: none; +} + +.sidebar li .tree .top, .sidebar li .tree .bottom { + display: block; + margin: 0; + padding: 0; + width: 7px; +} + +.sidebar li .tree .top { + border-left: 1px solid #aaa; + border-bottom: 1px solid #aaa; + height: 19px; +} + +.sidebar li .tree .bottom { + height: 22px; +} + +.sidebar li.branch .tree .bottom { + border-left: 1px solid #aaa; +} + +.sidebar.primary li.current a { + border-left: 3px solid #a33; + color: #a33; +} + +.sidebar.secondary li.current a { + border-left: 3px solid #33a; + color: #33a; +} + +.namespace-index h2 { + margin: 30px 0 0 0; +} + +.namespace-index h3 { + font-size: 16px; + font-weight: bold; + margin-bottom: 0; +} + +.namespace-index .topics { + padding-left: 30px; + margin: 11px 0 0 0; +} + +.namespace-index .topics li { + padding: 5px 0; +} + +.namespace-docs h3 { + font-size: 18px; + font-weight: bold; +} + +.public h3 { + margin: 0; + float: left; +} + +.usage { + clear: both; +} + +.public { + margin: 0; + border-top: 1px solid #e0e0e0; + padding-top: 14px; + padding-bottom: 6px; +} + +.public:last-child { + margin-bottom: 20%; +} + +.members .public:last-child { + margin-bottom: 0; +} + +.members { + margin: 15px 0; +} + +.members h4 { + color: #555; + font-weight: normal; + font-variant: small-caps; + margin: 0 0 5px 0; +} + +.members .inner { + padding-top: 5px; + padding-left: 12px; + margin-top: 2px; + margin-left: 7px; + border-left: 1px solid #bbb; +} + +#content .members .inner h3 { + font-size: 12pt; +} + +.members .public { + border-top: none; + margin-top: 0; + padding-top: 6px; + padding-bottom: 0; +} + +.members .public:first-child { + padding-top: 0; +} + +h4.type, +h4.dynamic, +h4.added, +h4.deprecated { + float: left; + margin: 3px 10px 15px 0; + font-size: 15px; + font-weight: bold; + font-variant: small-caps; +} + +.public h4.type, +.public h4.dynamic, +.public h4.added, +.public h4.deprecated { + font-size: 13px; + font-weight: bold; + margin: 3px 0 0 10px; +} + +.members h4.type, +.members h4.added, +.members h4.deprecated { + margin-top: 1px; +} + +h4.type { + color: #717171; +} + +h4.dynamic { + color: #9933aa; +} + +h4.added { + color: #508820; +} + +h4.deprecated { + color: #880000; +} + +.namespace { + margin-bottom: 30px; +} + +.namespace:last-child { + margin-bottom: 10%; +} + +.index { + padding: 0; + font-size: 80%; + margin: 15px 0; + line-height: 16px; +} + +.index * { + display: inline; +} + +.index p { + padding-right: 3px; +} + +.index li { + padding-right: 5px; +} + +.index ul { + padding-left: 0; +} + +.type-sig { + clear: both; + color: #088; +} + +.type-sig pre { + padding-top: 10px; + margin: 0; +} + +.usage code { + display: block; + color: #008; + margin: 2px 0; +} + +.usage code:first-child { + padding-top: 10px; +} + +p { + margin: 15px 0; +} + +.public p:first-child, .public pre.plaintext { + margin-top: 12px; +} + +.doc { + margin: 0 0 26px 0; + clear: both; +} + +.public .doc { + margin: 0; +} + +.namespace-index .doc { + margin-bottom: 20px; +} + +.namespace-index .namespace .doc { + margin-bottom: 10px; +} + +.markdown p, .markdown li, .markdown dt, .markdown dd, .markdown td { + line-height: 22px; +} + +.markdown li { + padding: 2px 0; +} + +.markdown h2 { + font-weight: normal; + font-size: 25px; + margin: 30px 0 10px 0; +} + +.markdown h3 { + font-weight: normal; + font-size: 20px; + margin: 30px 0 0 0; +} + +.markdown h4 { + font-size: 15px; + margin: 22px 0 -4px 0; +} + +.doc, .public, .namespace .index { + max-width: 680px; + overflow-x: visible; +} + +.markdown pre > code { + display: block; + padding: 10px; +} + +.markdown pre > code, .src-link a { + border: 1px solid #e4e4e4; + border-radius: 2px; +} + +.markdown code:not(.hljs), .src-link a { + background: #f6f6f6; +} + +pre.deps { + display: inline-block; + margin: 0 10px; + border: 1px solid #e4e4e4; + border-radius: 2px; + padding: 10px; + background-color: #f6f6f6; +} + +.markdown hr { + border-style: solid; + border-top: none; + color: #ccc; +} + +.doc ul, .doc ol { + padding-left: 30px; +} + +.doc table { + border-collapse: collapse; + margin: 0 10px; +} + +.doc table td, .doc table th { + border: 1px solid #dddddd; + padding: 4px 6px; +} + +.doc table th { + background: #f2f2f2; +} + +.doc dl { + margin: 0 10px 20px 10px; +} + +.doc dl dt { + font-weight: bold; + margin: 0; + padding: 3px 0; + border-bottom: 1px solid #ddd; +} + +.doc dl dd { + padding: 5px 0; + margin: 0 0 5px 10px; +} + +.doc abbr { + border-bottom: 1px dotted #333; + font-variant: none; + cursor: help; +} + +.src-link { + margin-bottom: 15px; +} + +.src-link a { + font-size: 70%; + padding: 1px 4px; + text-decoration: none; + color: #5555bb; +} diff --git a/docs/0.5.0-rc2/css/highlight.css b/docs/0.5.0-rc2/css/highlight.css new file mode 100644 index 00000000..d0cdaa30 --- /dev/null +++ b/docs/0.5.0-rc2/css/highlight.css @@ -0,0 +1,97 @@ +/* +github.com style (c) Vasily Polovnyov +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #333; + background: #f8f8f8; +} + +.hljs-comment, +.hljs-quote { + color: #998; + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-subst { + color: #333; + font-weight: bold; +} + +.hljs-number, +.hljs-literal, +.hljs-variable, +.hljs-template-variable, +.hljs-tag .hljs-attr { + color: #008080; +} + +.hljs-string, +.hljs-doctag { + color: #d14; +} + +.hljs-title, +.hljs-section, +.hljs-selector-id { + color: #900; + font-weight: bold; +} + +.hljs-subst { + font-weight: normal; +} + +.hljs-type, +.hljs-class .hljs-title { + color: #458; + font-weight: bold; +} + +.hljs-tag, +.hljs-name, +.hljs-attribute { + color: #000080; + font-weight: normal; +} + +.hljs-regexp, +.hljs-link { + color: #009926; +} + +.hljs-symbol, +.hljs-bullet { + color: #990073; +} + +.hljs-built_in, +.hljs-builtin-name { + color: #0086b3; +} + +.hljs-meta { + color: #999; + font-weight: bold; +} + +.hljs-deletion { + background: #fdd; +} + +.hljs-addition { + background: #dfd; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} diff --git a/docs/0.5.0-rc2/css/xiana.css b/docs/0.5.0-rc2/css/xiana.css new file mode 100644 index 00000000..19f79371 --- /dev/null +++ b/docs/0.5.0-rc2/css/xiana.css @@ -0,0 +1,18 @@ +.sidebar.primary { + background: #f8cc00; + width: 200px; +} + +.sidebar.primary::-webkit-scrollbar { + display: none; +} + +.logo { + padding-top: 10px; + margin:0px; +} + +.logo img { + height: 65px; +} + diff --git a/docs/0.5.0-rc2/getting-started.html b/docs/0.5.0-rc2/getting-started.html new file mode 100644 index 00000000..0b3899eb --- /dev/null +++ b/docs/0.5.0-rc2/getting-started.html @@ -0,0 +1,194 @@ + +How to make a Todo app using Xiana

+

How to make a Todo app using Xiana

+

Requirements

+
    +
  • docker
  • +
  • docker-compose
  • +
+

Create a new app

+

1. Use Xiana template to create project scaffold

+

Run the following code in your terminal

+
lein new xiana todo-app
+
+

This will create a directory todo-app containing project scaffold.

+

2. Satisfy dependencies

+

Run the following command from the newly created project directory to fetch frontend dependencies

+
lein shadow npm-deps
+
+

3. Run dockerized database

+

You need to have docker and docker-compose installed on your machine for the following command to work. Run the following from the root directory of your project.

+
docker-compose up -d
+
+

This should spin up a PostgreSQL database on port 5433. Name of the DB is todo_app You can verify that the database is running by connecting to it. Value of username and password is postgres.

+
docker exec -it todo-app_db_1 psql -U postgres -d todo_app
+
+

which should open a PostgreSQL shell if successful.

+

4. Populate database with data

+

On application start, the framework will look for database migrations located in the configured location. By default, this location is set to resources/migrations directory*.*

+

It is possible to create migrations by running from the project directory

+
lein migrate create todos
+
+

This will create two migration files inside the resources/migrations directory. Both file names will be prefixed by the timestamp value of the file creation.

+

Put the following SQL code inside the todos-up.sql file

+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+--;;
+CREATE TABLE todos
+(
+    id uuid NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
+    label varchar(2048) NOT NULL,
+    done boolean NOT NULL DEFAULT false,
+    created_at timestamptz NOT NULL DEFAULT now()
+);
+
+--;;
+
+INSERT INTO todos (label) values ('example label from DB');
+
+

and this one into the todos-down.sql file.

+
DROP TABLE todos;
+
+

5. Start the application

+
    +
  1. Run Clojure REPL and load contents of file dev/user.clj into it.
  2. +
  3. Execute function start-dev-system
  4. +
  5. Your Todo App should now be running. You can verify it by visiting URL http://localhost:3000/re-frame in your browser.
  6. +
+

6. Add endpoint to backend API

+

Routes definition can be found in the src/backend/todo_app/core.clj file. Replace the routes definition with following code.

+
(def routes
+  [["/api/todos" {:action #'fetch}]])
+
+

define a function to be called when the endpoint is hit.

+
(defn fetch
+  [state]
+  state)
+
+

Now you need to reload the changed files to REPL and restart the application by executing start-dev-system function once again. Now opening http://localhost:3000/api/todos in your browser returns a blank page.

+

7. Hello World!

+

Change the implementation of fetch function to display “Hello World!” every time our endpoint is hit.

+
(defn fetch
+  [state]
+  (assoc state :response {:status 200
+                          :body   "Hello World!"}))
+
+

Reload the modified function and restart the application.

+

8. Return contents of the todos table

+

Change the implementation of function fetch once again and create a new view function.

+

Function:

+
    +
  • fetch - now contains SQL query and a reference to a newly created view function.
  • +
  • view - describes a transformation of the data returned from the database to the form returned by server response.
  • +
+
(defn view
+  [{{db-data :db-data} :response-data :as state}]
+  (assoc state :response {:status 200
+                          :body   (mapv :todos/label db-data)}))
+(defn fetch
+  [state]
+  (assoc state
+    :view view
+    :query {:select [:*] :from [:todos]}))
+
+

Again, reload the modified function and restart the application.

+

After running following curl command from your shell, live data from your database should appear on your screen.

+
curl http://localhost:3000/api/todos
+
+

Create a page in the app to display data returned by the endpoint

+

1. Add frontend dependencies

+

Add following dependencies into project.clj file.

+
(defproject
+  ...
+  :dependencies [
+                 ...
+                 [cljs-ajax "0.8.4"]
+                 [day8.re-frame/http-fx "0.2.3"]
+                 ...
+                 ]
+  ...
+  )
+
+

2. Set initial value of re-frame database

+

File src/frontend/todo_app/db.cljs contains initial value of re-frame databse. Inside of this file replace value of * default-db* to following:

+
(def default-db
+  {:todos []})
+
+

3. Define re-frame events

+

Replace contents of src/frontend/todo_app/events.cljs file with the following code:

+
(ns todo-app.events
+  (:require
+    [re-frame.core :as re-frame]
+    [ajax.core :as ajax]
+    [day8.re-frame.http-fx]
+    [todo-app.db :as db]
+    ))
+
+(defn url [tail] (str "http://localhost:3000" tail))
+
+(re-frame/reg-event-db
+  ::initialize-db
+  (fn [_ _]
+    db/default-db))
+
+(re-frame/reg-event-db
+  ::add-todos->db
+  (fn [db [_ response]]
+    (assoc db :todos response)))
+
+(re-frame/reg-event-db
+  ::failure
+  (fn [db _]
+    (js/console.error "Something is wrong!")
+    db))
+
+(re-frame/reg-event-fx
+  ::fetch-todos!
+  (fn [_ [_]]
+    (js/console.info "Fetching todos!")
+    {:http-xhrio {:uri             (url "/api/todos")
+                  :response-format (ajax/json-response-format {:keywords? true})
+                  :format          (ajax/json-request-format)
+                  :on-success      [::add-todos->db]
+                  :on-failure      [::failure]}}))
+
+

4. Define re-frame subscription

+

Replace contents of src/frontend/todo_app/subs.cljs file with the following code:

+
(ns todo-app.subs
+  (:require
+    [re-frame.core :as re-frame]))
+
+(re-frame/reg-sub
+  ::todos
+  (fn [db]
+    (:todos db)))
+
+

5. Define page view

+

Replace contents of src/frontend/todo_app/views.cljs file with the following code:

+
(ns todo-app.views
+  (:require
+    [re-frame.core :as re-frame]
+    [todo-app.events :as events]
+    [todo-app.subs :as subs]))
+
+(defn main-panel []
+  (re-frame/dispatch [::events/fetch-todos!])
+  (let [todos (re-frame/subscribe [::subs/todos])]
+    [:div
+     (map #(identity [:ul %])
+          @todos)]))
+
+

6. Define frontend routes

+

Update routes definition in file src/backend/todo_app/core.clj to following:

+
(def routes
+  [["/todos" {:action #'re-frame/handle-index}]
+   ["/assets/*" (ring/create-resource-handler {:path "/"})]
+   ["/api" {}
+    ["/todos" {:get {:action #'fetch}}]]])
+
+

7. At this point the app should be running

+

Again, reload the modified code and restart the application. After opening http://localhost:3000/todos in your browser, it returns a page containing the data from our database that looks like this:

+

success

+

Create another endpoint to add a new entry to the DB

+

Left as an exercise for the reader.

+
\ No newline at end of file diff --git a/docs/0.5.0-rc2/how-to.html b/docs/0.5.0-rc2/how-to.html new file mode 100644 index 00000000..27fa2978 --- /dev/null +++ b/docs/0.5.0-rc2/how-to.html @@ -0,0 +1,383 @@ + +How to

+

How to

+ +

Defining new interceptors

+

The interceptor is a map, can have three functions like:

+

:enter Runs while we are going down from the request to it’s action, in the order of executors

+

:leave Runs while we’re going up from the action to the response.

+

:error Executed when any error thrown while executing the two other functions

+

and a :name can be defined. All keys are optional, and if it missing it’s replaced by identity.

+

The provided functions are should have one parameter, the application state, and should return the modified state.

+

Interceptor example

+

+{:name  :sample-interceptor
+ :enter (fn [state]
+          (println "Enter: " state)
+          (-> state
+              (transform-somehow)
+              (or-do-side-effects)))
+ :leave (fn [state]
+          (println "Leave: " state)
+          state)
+ :error (fn [state]
+          (println "Error: " state)
+          ;; Here `state` should have previously thrown exception
+          ;; stored in `:error` key.
+          ;; you can do something useful with it (e.g. log it)
+          ;; and/or handle it by `dissoc`ing from the state.
+          ;; In that case remaining `leave` interceptors will be executed.
+          (assoc state :response {:status 500 :body "Error occurred while printing out state"}))}
+
+

Providing default interceptors

+

The router and controller interceptors definition is part of the application startup. The system’s dependency map should contain two sequence of interceptors like

+
(def app-cfg
+  {:routes                  routes
+   :router-interceptors     [...]
+   :controller-interceptors [...]
+   :web-socket-interceptors [...]})
+
+

Interceptor overriding

+

On route definition you can interfere with the default controller interceptors. With the route definition you are able to set up different controller interceptors other than the ones already defined with the app. There are three ways to do it:

+
... {:action       #(do something)
+     :interceptors [...]}
+
+

will override all controller interceptors

+
... {:action       #(do something)
+     :interceptors {:around [...]}}
+
+

will extend the defaults around

+
... {:action       #(do something)
+     :interceptors {:inside [...]}}
+
+

will extend the defaults inside

+
... {:action       #(do something)
+     :interceptors {:inside [...]
+                    :around [...]}}
+
+

will extend the defaults inside and around

+
... {:action       #(do something)
+     :interceptors {:except [...]}}
+
+

will skip the excepted interceptors from defaults

+

Role based access and data ownership control

+

To get the benefits of tiny RBAC library you need to provide the resource and the action for your endpoint in router definition:

+
[["/api"
+  ["/image" {:delete {:action     delete-action
+                      :permission :image/delete}}]]]
+
+

and add your role-set into your app’s dependencies:

+
(defn ->system
+  [app-cfg]
+  (-> (config/config app-cfg)
+      xiana.rbac/init
+      ws/start))
+
+

On :enter, the interceptor performs the permission check. It determines if the action allowed for the user found in (-> state :session-data :user). If access to the resource/action isn’t permitted, then the response is:

+
{:status 403
+ :body   "Forbidden"}
+
+

If a permission is found, then it goes into (-> state :request-data :user-permissions) as a parameter for data ownership processing.

+

On :leave, executes the restriction function found in (-> state :request-data :restriction-fn). The restriction-fn should look like this:

+
(defn restriction-fn
+  [state]
+  (let [user-permissions (get-in state [:request-data :user-permissions])]
+    (cond
+      (user-permissions :image/all) state
+      (user-permissions :image/own) (let [session-id (get-in state [:request :headers "session-id"])
+                                          session-backend (-> state :deps :session-backend)
+                                          user-id (:users/id (session/fetch session-backend session-id))]
+                                      (update state :query sql/merge-where [:= :owner.id user-id])))))
+
+

The rbac interceptor must be placed between the action and the db-access interceptors in the interceptor chain.

+

Access and data ownership control

+

RBAC is a handy way to restrict user actions on different resources. It’s a role-based access control and helps you to implement data ownership control. The rbac/interceptor should be placed inside db-access.

+

Role set definition

+

For tiny-RBAC you should provide a role-set. It’s a map which defines the application resources, the actions on it, the roles with the different granted actions, and restrictions for data ownership control. This map must be placed in deps.

+

Here’s an example role-set for an image service:

+
(def role-set
+  (-> (b/add-resource {} :image)
+      (b/add-action :image [:upload :download :delete])
+      (b/add-role :guest)
+      (b/add-inheritance :member :guest)
+      (b/add-permission :guest :image :download :all)
+      (b/add-permission :member :image :upload :all)
+      (b/add-permission :member :image :delete :own)))
+
+

It defines a role-set with:

+
    +
  • an :image resource,
  • +
  • :upload :download :delete actions on :image resource
  • +
  • a :guest role, who can download all the images
  • +
  • a :member role, who inherits all of :guest’s roles, can upload :all images, and delete :own images.
  • +
+

Provide resource/action at routing

+

The resource and action can be defined on route definition. The RBAC interceptor will check permissions against what is defined here:

+
(def routes
+  [["/api" {:handler handler-fn}
+    ["/image" {:get    {:action     get-image
+                        :permission :image/download}
+               :put    {:action     add-image
+                        :permission :image/upload}
+               :delete {:action     delete-image
+                        :permission :image/delete}}]]])
+
+

Application start-up

+
(def role-set
+  (-> (b/add-resource {} :image)
+      (b/add-action :image [:upload :download :delete])
+      (b/add-role :guest)
+      (b/add-inheritance :member :guest)
+      (b/add-permission :guest :image :download :all)
+      (b/add-permission :member :image :upload :all)
+      (b/add-permission :member :image :delete :own)))
+
+(def routes
+  [["/api" {:handler handler-fn}
+    ["/login" {:action       login
+               :interceptors {:except [session/interceptor]}}]
+    ["/image" {:get    {:action     get-image
+                        :permission :image/download}
+               :put    {:action     add-image
+                        :permission :image/upload}
+               :delete {:action     delete-image
+                        :permission :image/delete}}]]])
+
+(defn ->system
+  [app-cfg]
+  (-> (config/config)
+      (merge app-cfg)
+      session-backend/init-backend
+      routes/reset
+      db-core/connect
+      db-core/migrate!
+      ws/start))
+
+(def app-cfg
+  {:routes                  routes
+   :role-set                role-set
+   :controller-interceptors [interceptors/params
+                             session/interceptor
+                             rbac/interceptor
+                             interceptors/db-access]})
+
+(defn -main
+  [& _args]
+  (->system app-cfg))
+
+
+

Access control

+

Prerequisites:

+
    +
  • role-set in (-> state :deps :role-set)
  • +
  • route definition has :permission key
  • +
  • user’s role is in (-> state :session-data :users/role)
  • +
+

If the :permission key is missing, all requests are going to be granted. If role-set or :users/role is missing, all requests are going to be denied.

+

When rbac/interceptor :enter is executed, it checks if the user has any permission on the pre-defined resource/action pair. If there is any, it collects all of them (including inherited permissions) into a set of format: :resource/restriction.

+

For example:

+
:image/own
+
+

means the given user is granted the permission to do the given action on :own :image resource. This will help you to implement data ownership functions. This set is associated in (-> state :request-data :user-permissions)

+

If user cannot perform the given action on the given resource (neither by inheritance nor by direct permission), the interceptor will interrupt the execution flow with the response:

+
{:status 403
+ :body   "Forbidden"}
+
+

Data ownership

+

Data ownership control is about restricting database results only to the elements on which the user is able to perform the given action. In the context of the example above, it means :members are able to delete only the owned :images. At this point, you can use the result of the access control from the state. Continuing with the same example.

+

From this generic query

+
{:delete [:*]
+ :from   [:images]
+ :where  [:= :id (get-in state [:params :image-id])]}
+
+

you want to switch to something like this:

+
{:delete [:*]
+ :from   [:images]
+ :where  [:and
+          [:= :id (get-in state [:params :image-id])]
+          [:= :owner.id user-id]]}
+
+

To achieve this, you can simply provide a restriction function into (-> state :request-data :restriction-fn) The user-permissions is a set, so it can be easily used for making conditions:

+
(defn restriction-fn
+  [state]
+  (let [user-permissions (get-in state [:request-data :user-permissions])]
+    (cond
+      (user-permissions :image/own) (let [user-id (get-in state [:session-data :users/id])]
+                                      (update state :query sql/merge-where [:= :owner.id user-id]))
+      :else state)))
+
+

And finally, the only missing piece of code: the model, and the action

+

+(defn delete-query
+  [state]
+  {:delete [:*]
+   :from   [:images]
+   :where  [:= :id (get-in state [:params :image-id])]})
+
+(defn delete-image
+  [state]
+  (-> state
+      (assoc :query (delete-query state))
+      (assoc-in [:request-data :restriction-fn] restriction-fn)))
+
+

Login implementation

+

Xiana framework does not have any login or logout functions, as every application has its own user management logic. Though Xiana offers all the tools to easily implement them. One of the default interceptors is the session interceptor. If included, it can validate a request only if the session already exists in session storage. To log in a user, simply add its session data to the storage. (TODO: where? What is the exact key to modify?). All sessions should have a unique UUID as session-id. The active session lives under (-> state :session-data). On every request, before reaching the action defined by the route, the interceptor checks [:headers :session-id] among other things. Which is the id of the current session. The session is then loaded in session storage. If the id is not found, the execution flow is interrupted with the response:

+
{:status 401
+ :body   "Invalid or missing session"}
+
+

To implement login, you need to use the session interceptor in

+
(let [;; Create a unique ID
+      session-id (UUID/randomUUID)]
+  ;; Store a new session in session storage
+  (add! session-storage session-id {:session-id session-id})
+  ;; Make sure session-id is part of the response
+  (assoc-in state [:response :headers :session-id] (str session-id)))
+
+

or use the guest-session interceptor, which creates a guest session for unknown, or missing sessions.

+

For role-based access control, you need to store the actual user in your session data. First, you’ll have to query it from the database. It is best placed in models/user namespace. Here’s an example:

+
(defn fetch-query
+  [state]
+  (let [login (-> state :request :body-params :login)]
+    (-> (select :*)
+        (from :users)
+        (where [:and
+                :is_active
+                [:or
+                 [:= :email login]
+                 [:= :username login]]]))))
+
+

To execute it, place db-access interceptor in the interceptors list. It injects the query result into the state. If you already have this injected, you can modify your create session function like this:

+
(let [;; Get user from database result
+      user (-> state :response-data :db-data first)
+      ;; Create session
+      session-id (UUID/randomUUID)]
+  ;; Store the new session in session storage. Notice the addition of user. 
+  (add! session-storage session-id (assoc user :session-id session-id))
+  ;; Make sure session-id is part of the response
+  (assoc-in state [:response :headers :session-id] (str session-id)))
+
+

Be sure to remove user’s password and any other sensitive information before storing it:

+
(let [;; Get user from database result
+      user (-> state
+               :response-data
+               :db-data
+               first
+               ;; Remove password for session storage
+               (dissoc :users/password))
+      ;; Create session id
+      session-id (UUID/randomUUID)]
+  ;; Store the new session in session storage
+  (add! session-storage session-id (assoc user :session-id session-id))
+  ;; Make sure session-id is part of the response
+  (assoc-in state [:response :headers :session-id] (str session-id)))
+
+

Next, we check if the credentials are correct, so we use an if statement.

+
(if (valid-credentials?)
+  (let [;; Get user from database result
+        user (-> state
+                 :response-data
+                 :db-data
+                 first
+                 ;; Remove password for session storage
+                 (dissoc :users/password))
+        ;; Create session ID
+        session-id (UUID/randomUUID)]
+    ;; Store the new session in session storage
+    (add! session-storage session-id (assoc user :session-id session-id))
+    ;; Make sure session-id is part of the response
+    (assoc-in state [:response :headers :session-id] (str session-id)))
+  (throw (ex-info "Missing session data"
+                  {:xiana/response
+                   {:body   "Login failed"
+                    :status 401}})))
+
+

Xiana provides xiana.hash to check user credentials:

+
(defn- valid-credentials?
+  "It checks that the password provided by the user matches the encrypted password from the database."
+  [state]
+  (let [user-provided-pass (-> state :request :body-params :password)
+        db-stored-pass (-> state :response-data :db-data first :users/password)]
+    (and user-provided-pass
+         db-stored-pass
+         (hash/check state user-provided-pass db-stored-pass))))
+
+

The login logic is done, but where to place it?

+

Do you remember the side effect interceptor? It’s running after we have the query result from the database, and before the final response is rendered with the view interceptor. The place for the function defined above is in the interceptor chain. How does it go there? Let’s see an action

+
(defn action
+  [state]
+  (assoc state :side-effect side-effects/login))
+
+

This is the place for injecting the database query, too:

+
(defn action
+  [state]
+  (assoc state :side-effect side-effects/login
+               :query model/fetch-query))
+
+

But some tiny thing is still missing. The definition of the response in the all-ok case. A happy path response.

+
(defn login-success
+  [state]
+  (let [id (-> state :response-data :db-data first :users/id)]
+    (-> state
+        (assoc-in [:response :body]
+                  {:view-type "login"
+                   :data      {:login   "succeed"
+                               :user-id id}})
+        (assoc-in [:response :status] 200))))
+
+

And finally the view is injected in the action function:

+
(defn action
+  [state]
+  (assoc state :side-effect side-effects/login
+               :view view/login-success
+               :query model/fetch-query))
+
+

Logout implementation

+

To do a logout is much easier than a login implementation. The session-interceptor does half of the work, and if you have a running session, then it will not complain. The only thing you should do is to remove the actual session from the state and from session storage. Something like this:

+
(defn logout
+  [state]
+  (let [session-store (get-in state [:deps :session-backend])
+        session-id (get-in state [:session-data :session-id])]
+    (session/delete! session-store session-id)
+    (dissoc state :session-data)))
+
+

Add the ok response

+
(defn logout-view
+  [state]
+  (-> state
+      (assoc-in [:response :body]
+                {:view-type "logout"
+                 :data      {:logout "succeed"}})
+      (assoc-in [:response :status] 200)))
+
+

and use it:

+
(defn logout
+  [state]
+  (let [session-store (get-in state [:deps :session-backend])
+        session-id (get-in state [:session-data :session-id])]
+    (session/delete! session-store session-id)
+    (-> state
+        (dissoc :session-data)
+        (assoc :view views/logout-view))))
+
+
\ No newline at end of file diff --git a/docs/0.5.0-rc2/index.html b/docs/0.5.0-rc2/index.html new file mode 100644 index 00000000..76b2d704 --- /dev/null +++ b/docs/0.5.0-rc2/index.html @@ -0,0 +1,3 @@ + +

Topics

Namespaces

xiana.coercion

Public variables and functions:

xiana.commons

Public variables and functions:

xiana.config

Public variables and functions:

xiana.cookies

Public variables and functions:

xiana.db

Public variables and functions:

xiana.handler

Public variables and functions:

xiana.hash

Public variables and functions:

xiana.interceptor.error

Public variables and functions:

xiana.interceptor.muuntaja

Public variables and functions:

xiana.interceptor.queue

Public variables and functions:

xiana.mail

Public variables and functions:

xiana.rbac

Public variables and functions:

xiana.route

Public variables and functions:

xiana.route.helpers

Public variables and functions:

xiana.scheduler

Public variables and functions:

xiana.state

Public variables and functions:

xiana.webserver

Public variables and functions:

xiana.websockets

Public variables and functions:

\ No newline at end of file diff --git a/docs/0.5.0-rc2/js/highlight.min.js b/docs/0.5.0-rc2/js/highlight.min.js new file mode 100644 index 00000000..6486ffd9 --- /dev/null +++ b/docs/0.5.0-rc2/js/highlight.min.js @@ -0,0 +1,2 @@ +/*! highlight.js v9.6.0 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var a=r?"":y.classPrefix,i='',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("clojure",function(e){var t={"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",o={b:n,r:0},s={cN:"number",b:a,r:0},i=e.inherit(e.QSM,{i:null}),c=e.C(";","$",{r:0}),d={cN:"literal",b:/\b(true|false|nil)\b/},l={b:"[\\[\\{]",e:"[\\]\\}]"},m={cN:"comment",b:"\\^"+n},p=e.C("\\^\\{","\\}"),u={cN:"symbol",b:"[:]{1,2}"+n},f={b:"\\(",e:"\\)"},h={eW:!0,r:0},y={k:t,l:n,cN:"name",b:n,starts:h},b=[f,i,m,p,c,u,l,s,d,o];return f.c=[e.C("comment",""),y,h],h.c=b,l.c=b,{aliases:["clj"],i:/\S/,c:[f,i,m,p,c,u,l,s,d]}});hljs.registerLanguage("clojure-repl",function(e){return{c:[{cN:"meta",b:/^([\w.-]+|\s*#_)=>/,starts:{e:/$/,sL:"clojure"}}]}}); \ No newline at end of file diff --git a/docs/0.5.0-rc2/js/jquery.min.js b/docs/0.5.0-rc2/js/jquery.min.js new file mode 100644 index 00000000..73f33fb3 --- /dev/null +++ b/docs/0.5.0-rc2/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.0 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k="".trim,l={},m="1.11.0",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(l.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:k&&!k.call("\ufeff\xa0")?function(a){return null==a?"":k.call(a)}:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||n.guid++,e):void 0},now:function(){return+new Date},support:l}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s="sizzle"+-new Date,t=a.document,u=0,v=0,w=eb(),x=eb(),y=eb(),z=function(a,b){return a===b&&(j=!0),0},A="undefined",B=1<<31,C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=D.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",M=L.replace("w","w#"),N="\\["+K+"*("+L+")"+K+"*(?:([*^$|!~]?=)"+K+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+M+")|)|)"+K+"*\\]",O=":("+L+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+N.replace(3,8)+")*)|.*)\\)|)",P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(O),U=new RegExp("^"+M+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L.replace("w","w*")+")"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=/'|\\/g,ab=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),bb=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{G.apply(D=H.call(t.childNodes),t.childNodes),D[t.childNodes.length].nodeType}catch(cb){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function db(a,b,d,e){var f,g,h,i,j,m,p,q,u,v;if((b?b.ownerDocument||b:t)!==l&&k(b),b=b||l,d=d||[],!a||"string"!=typeof a)return d;if(1!==(i=b.nodeType)&&9!==i)return[];if(n&&!e){if(f=Z.exec(a))if(h=f[1]){if(9===i){if(g=b.getElementById(h),!g||!g.parentNode)return d;if(g.id===h)return d.push(g),d}else if(b.ownerDocument&&(g=b.ownerDocument.getElementById(h))&&r(b,g)&&g.id===h)return d.push(g),d}else{if(f[2])return G.apply(d,b.getElementsByTagName(a)),d;if((h=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(h)),d}if(c.qsa&&(!o||!o.test(a))){if(q=p=s,u=b,v=9===i&&a,1===i&&"object"!==b.nodeName.toLowerCase()){m=ob(a),(p=b.getAttribute("id"))?q=p.replace(_,"\\$&"):b.setAttribute("id",q),q="[id='"+q+"'] ",j=m.length;while(j--)m[j]=q+pb(m[j]);u=$.test(a)&&mb(b.parentNode)||b,v=m.join(",")}if(v)try{return G.apply(d,u.querySelectorAll(v)),d}catch(w){}finally{p||b.removeAttribute("id")}}}return xb(a.replace(P,"$1"),b,d,e)}function eb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function fb(a){return a[s]=!0,a}function gb(a){var b=l.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function hb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function ib(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||B)-(~a.sourceIndex||B);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function jb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function kb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function lb(a){return fb(function(b){return b=+b,fb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function mb(a){return a&&typeof a.getElementsByTagName!==A&&a}c=db.support={},f=db.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},k=db.setDocument=function(a){var b,e=a?a.ownerDocument||a:t,g=e.defaultView;return e!==l&&9===e.nodeType&&e.documentElement?(l=e,m=e.documentElement,n=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){k()},!1):g.attachEvent&&g.attachEvent("onunload",function(){k()})),c.attributes=gb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=gb(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(e.getElementsByClassName)&&gb(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=gb(function(a){return m.appendChild(a).id=s,!e.getElementsByName||!e.getElementsByName(s).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==A&&n){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){var c=typeof a.getAttributeNode!==A&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==A?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==A&&n?b.getElementsByClassName(a):void 0},p=[],o=[],(c.qsa=Y.test(e.querySelectorAll))&&(gb(function(a){a.innerHTML="",a.querySelectorAll("[t^='']").length&&o.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||o.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll(":checked").length||o.push(":checked")}),gb(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&o.push("name"+K+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||o.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),o.push(",.*:")})),(c.matchesSelector=Y.test(q=m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&gb(function(a){c.disconnectedMatch=q.call(a,"div"),q.call(a,"[s!='']:x"),p.push("!=",O)}),o=o.length&&new RegExp(o.join("|")),p=p.length&&new RegExp(p.join("|")),b=Y.test(m.compareDocumentPosition),r=b||Y.test(m.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},z=b?function(a,b){if(a===b)return j=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===t&&r(t,a)?-1:b===e||b.ownerDocument===t&&r(t,b)?1:i?I.call(i,a)-I.call(i,b):0:4&d?-1:1)}:function(a,b){if(a===b)return j=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],k=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:i?I.call(i,a)-I.call(i,b):0;if(f===g)return ib(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)k.unshift(c);while(h[d]===k[d])d++;return d?ib(h[d],k[d]):h[d]===t?-1:k[d]===t?1:0},e):l},db.matches=function(a,b){return db(a,null,null,b)},db.matchesSelector=function(a,b){if((a.ownerDocument||a)!==l&&k(a),b=b.replace(S,"='$1']"),!(!c.matchesSelector||!n||p&&p.test(b)||o&&o.test(b)))try{var d=q.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return db(b,l,null,[a]).length>0},db.contains=function(a,b){return(a.ownerDocument||a)!==l&&k(a),r(a,b)},db.attr=function(a,b){(a.ownerDocument||a)!==l&&k(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!n):void 0;return void 0!==f?f:c.attributes||!n?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},db.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},db.uniqueSort=function(a){var b,d=[],e=0,f=0;if(j=!c.detectDuplicates,i=!c.sortStable&&a.slice(0),a.sort(z),j){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return i=null,a},e=db.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=db.selectors={cacheLength:50,createPseudo:fb,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ab,bb),a[3]=(a[4]||a[5]||"").replace(ab,bb),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||db.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&db.error(a[0]),a},PSEUDO:function(a){var b,c=!a[5]&&a[2];return V.CHILD.test(a[0])?null:(a[3]&&void 0!==a[4]?a[2]=a[4]:c&&T.test(c)&&(b=ob(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ab,bb).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=w[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&w(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==A&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=db.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),t=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&t){k=q[s]||(q[s]={}),j=k[a]||[],n=j[0]===u&&j[1],m=j[0]===u&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[u,n,m];break}}else if(t&&(j=(b[s]||(b[s]={}))[a])&&j[0]===u)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(t&&((l[s]||(l[s]={}))[a]=[u,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||db.error("unsupported pseudo: "+a);return e[s]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?fb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:fb(function(a){var b=[],c=[],d=g(a.replace(P,"$1"));return d[s]?fb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:fb(function(a){return function(b){return db(a,b).length>0}}),contains:fb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:fb(function(a){return U.test(a||"")||db.error("unsupported lang: "+a),a=a.replace(ab,bb).toLowerCase(),function(b){var c;do if(c=n?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===m},focus:function(a){return a===l.activeElement&&(!l.hasFocus||l.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:lb(function(){return[0]}),last:lb(function(a,b){return[b-1]}),eq:lb(function(a,b,c){return[0>c?c+b:c]}),even:lb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:lb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:lb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:lb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function qb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=v++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[u,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[s]||(b[s]={}),(h=i[d])&&h[0]===u&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function rb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function sb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function tb(a,b,c,d,e,f){return d&&!d[s]&&(d=tb(d)),e&&!e[s]&&(e=tb(e,f)),fb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||wb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:sb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=sb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=sb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ub(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],i=g||d.relative[" "],j=g?1:0,k=qb(function(a){return a===b},i,!0),l=qb(function(a){return I.call(b,a)>-1},i,!0),m=[function(a,c,d){return!g&&(d||c!==h)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>j;j++)if(c=d.relative[a[j].type])m=[qb(rb(m),c)];else{if(c=d.filter[a[j].type].apply(null,a[j].matches),c[s]){for(e=++j;f>e;e++)if(d.relative[a[e].type])break;return tb(j>1&&rb(m),j>1&&pb(a.slice(0,j-1).concat({value:" "===a[j-2].type?"*":""})).replace(P,"$1"),c,e>j&&ub(a.slice(j,e)),f>e&&ub(a=a.slice(e)),f>e&&pb(a))}m.push(c)}return rb(m)}function vb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,i,j,k){var m,n,o,p=0,q="0",r=f&&[],s=[],t=h,v=f||e&&d.find.TAG("*",k),w=u+=null==t?1:Math.random()||.1,x=v.length;for(k&&(h=g!==l&&g);q!==x&&null!=(m=v[q]);q++){if(e&&m){n=0;while(o=a[n++])if(o(m,g,i)){j.push(m);break}k&&(u=w)}c&&((m=!o&&m)&&p--,f&&r.push(m))}if(p+=q,c&&q!==p){n=0;while(o=b[n++])o(r,s,g,i);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=E.call(j));s=sb(s)}G.apply(j,s),k&&!f&&s.length>0&&p+b.length>1&&db.uniqueSort(j)}return k&&(u=w,h=t),r};return c?fb(f):f}g=db.compile=function(a,b){var c,d=[],e=[],f=y[a+" "];if(!f){b||(b=ob(a)),c=b.length;while(c--)f=ub(b[c]),f[s]?d.push(f):e.push(f);f=y(a,vb(e,d))}return f};function wb(a,b,c){for(var d=0,e=b.length;e>d;d++)db(a,b[d],c);return c}function xb(a,b,e,f){var h,i,j,k,l,m=ob(a);if(!f&&1===m.length){if(i=m[0]=m[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&c.getById&&9===b.nodeType&&n&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(ab,bb),b)||[])[0],!b)return e;a=a.slice(i.shift().value.length)}h=V.needsContext.test(a)?0:i.length;while(h--){if(j=i[h],d.relative[k=j.type])break;if((l=d.find[k])&&(f=l(j.matches[0].replace(ab,bb),$.test(i[0].type)&&mb(b.parentNode)||b))){if(i.splice(h,1),a=f.length&&pb(i),!a)return G.apply(e,f),e;break}}}return g(a,m)(f,b,!n,e,$.test(a)&&mb(b.parentNode)||b),e}return c.sortStable=s.split("").sort(z).join("")===s,c.detectDuplicates=!!j,k(),c.sortDetached=gb(function(a){return 1&a.compareDocumentPosition(l.createElement("div"))}),gb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||hb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&gb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||hb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),gb(function(a){return null==a.getAttribute("disabled")})||hb(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),db}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=a.document,A=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,B=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:A.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:z,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=z.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return y.find(a);this.length=1,this[0]=d}return this.context=z,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};B.prototype=n.fn,y=n(z);var C=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!n(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function E(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return E(a,"nextSibling")},prev:function(a){return E(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(D[a]||(e=n.unique(e)),C.test(a)&&(e=e.reverse())),this.pushStack(e)}});var F=/\S+/g,G={};function H(a){var b=G[a]={};return n.each(a.match(F)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?G[a]||H(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&n.each(arguments,function(a,c){var d;while((d=n.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){if(a===!0?!--n.readyWait:!n.isReady){if(!z.body)return setTimeout(n.ready);n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(z,[n]),n.fn.trigger&&n(z).trigger("ready").off("ready"))}}});function J(){z.addEventListener?(z.removeEventListener("DOMContentLoaded",K,!1),a.removeEventListener("load",K,!1)):(z.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(z.addEventListener||"load"===event.type||"complete"===z.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===z.readyState)setTimeout(n.ready);else if(z.addEventListener)z.addEventListener("DOMContentLoaded",K,!1),a.addEventListener("load",K,!1);else{z.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&z.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!n.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}J(),n.ready()}}()}return I.promise(b)};var L="undefined",M;for(M in n(l))break;l.ownLast="0"!==M,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c=z.getElementsByTagName("body")[0];c&&(a=z.createElement("div"),a.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",b=z.createElement("div"),c.appendChild(a).appendChild(b),typeof b.style.zoom!==L&&(b.style.cssText="border:0;margin:0;width:1px;padding:1px;display:inline;zoom:1",(l.inlineBlockNeedsLayout=3===b.offsetWidth)&&(c.style.zoom=1)),c.removeChild(a),a=b=null)}),function(){var a=z.createElement("div");if(null==l.deleteExpando){l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}}a=null}(),n.acceptData=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(n.acceptData(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f +}}function S(a,b,c){if(n.acceptData(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d]));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},X=/^(?:checkbox|radio)$/i;!function(){var a=z.createDocumentFragment(),b=z.createElement("div"),c=z.createElement("input");if(b.setAttribute("className","t"),b.innerHTML="
a",l.leadingWhitespace=3===b.firstChild.nodeType,l.tbody=!b.getElementsByTagName("tbody").length,l.htmlSerialize=!!b.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==z.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,a.appendChild(c),l.appendChecked=c.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,a.appendChild(b),b.innerHTML="",l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){l.noCloneEvent=!1}),b.cloneNode(!0).click()),null==l.deleteExpando){l.deleteExpando=!0;try{delete b.test}catch(d){l.deleteExpando=!1}}a=b=c=null}(),function(){var b,c,d=z.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),l[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var Y=/^(?:input|select|textarea)$/i,Z=/^key/,$=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,ab=/^([^.]*)(?:\.(.+)|)$/;function bb(){return!0}function cb(){return!1}function db(){try{return z.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof n===L||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(F)||[""],h=b.length;while(h--)f=ab.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(F)||[""],j=b.length;while(j--)if(h=ab.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,m,o=[d||z],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||z,3!==d.nodeType&&8!==d.nodeType&&!_.test(p+n.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[n.expando]?b:new n.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),k=n.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!n.isWindow(d)){for(i=k.delegateType||p,_.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||z)&&o.push(l.defaultView||l.parentWindow||a)}m=0;while((h=o[m++])&&!b.isPropagationStopped())b.type=m>1?i:k.bindType||p,f=(n._data(h,"events")||{})[b.type]&&n._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&n.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&n.acceptData(d)&&g&&d[p]&&!n.isWindow(d)){l=d[g],l&&(d[g]=null),n.event.triggered=p;try{d[p]()}catch(r){}n.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((n.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?n(c,this).index(i)>=0:n.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ib=/^\s+/,jb=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,kb=/<([\w:]+)/,lb=/\s*$/g,sb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]},tb=eb(z),ub=tb.appendChild(z.createElement("div"));sb.optgroup=sb.option,sb.tbody=sb.tfoot=sb.colgroup=sb.caption=sb.thead,sb.th=sb.td;function vb(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==L?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==L?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,vb(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function wb(a){X.test(a.type)&&(a.defaultChecked=a.checked)}function xb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function yb(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function zb(a){var b=qb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ab(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}function Bb(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Cb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(yb(b).text=a.text,zb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&X.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}n.extend({clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!hb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ub.innerHTML=a.outerHTML,ub.removeChild(f=ub.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=vb(f),h=vb(a),g=0;null!=(e=h[g]);++g)d[g]&&Cb(e,d[g]);if(b)if(c)for(h=h||vb(a),d=d||vb(f),g=0;null!=(e=h[g]);g++)Bb(e,d[g]);else Bb(a,f);return d=vb(f,"script"),d.length>0&&Ab(d,!i&&vb(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k,m=a.length,o=eb(b),p=[],q=0;m>q;q++)if(f=a[q],f||0===f)if("object"===n.type(f))n.merge(p,f.nodeType?[f]:f);else if(mb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(kb.exec(f)||["",""])[1].toLowerCase(),k=sb[i]||sb._default,h.innerHTML=k[1]+f.replace(jb,"<$1>")+k[2],e=k[0];while(e--)h=h.lastChild;if(!l.leadingWhitespace&&ib.test(f)&&p.push(b.createTextNode(ib.exec(f)[0])),!l.tbody){f="table"!==i||lb.test(f)?""!==k[1]||lb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)n.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}n.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),l.appendChecked||n.grep(vb(p,"input"),wb),q=0;while(f=p[q++])if((!d||-1===n.inArray(f,d))&&(g=n.contains(f.ownerDocument,f),h=vb(o.appendChild(f),"script"),g&&Ab(h),c)){e=0;while(f=h[e++])pb.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.deleteExpando,m=n.event.special;null!=(d=a[h]);h++)if((b||n.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k?delete d[i]:typeof d.removeAttribute!==L?d.removeAttribute(i):d[i]=null,c.push(f))}}}),n.fn.extend({text:function(a){return W(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||z).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(vb(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&Ab(vb(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(vb(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return W(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(gb,""):void 0;if(!("string"!=typeof a||nb.test(a)||!l.htmlSerialize&&hb.test(a)||!l.leadingWhitespace&&ib.test(a)||sb[(kb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(jb,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(vb(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(vb(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,k=this.length,m=this,o=k-1,p=a[0],q=n.isFunction(p);if(q||k>1&&"string"==typeof p&&!l.checkClone&&ob.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(k&&(i=n.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=n.map(vb(i,"script"),yb),f=g.length;k>j;j++)d=i,j!==o&&(d=n.clone(d,!0,!0),f&&n.merge(g,vb(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,n.map(g,zb),j=0;f>j;j++)d=g[j],pb.test(d.type||"")&&!n._data(d,"globalEval")&&n.contains(h,d)&&(d.src?n._evalUrl&&n._evalUrl(d.src):n.globalEval((d.text||d.textContent||d.innerHTML||"").replace(rb,"")));i=c=null}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],g=n(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Db,Eb={};function Fb(b,c){var d=n(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:n.css(d[0],"display");return d.detach(),e}function Gb(a){var b=z,c=Eb[a];return c||(c=Fb(a,b),"none"!==c&&c||(Db=(Db||n("