├── .editorconfig ├── .github └── workflows │ └── crystal.yml ├── .gitignore ├── .tool-versions ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── examples ├── base.cr └── simple-server.cr ├── orion-banner.svg ├── shard.yml ├── spec ├── fixtures │ ├── index.html │ └── test.txt ├── orion │ ├── dsl │ │ ├── concerns_spec.cr │ │ ├── constraints_spec.cr │ │ ├── handlers_spec.cr │ │ ├── helpers_spec.cr │ │ ├── match_spec.cr │ │ ├── methods_spec.cr │ │ ├── resources_spec.cr │ │ ├── scope_spec.cr │ │ └── websockets_spec.cr │ ├── handlers │ │ └── method_override_param_spec.cr │ └── router_spec.cr └── spec_helper.cr └── src ├── app.cr ├── http.cr ├── http └── request.cr ├── macro.cr ├── orion.cr ├── orion ├── CHANGELOG.md ├── action.cr ├── assets.cr ├── assets │ └── pack.cr ├── cache.cr ├── cache │ └── keyable.cr ├── config.cr ├── constraint.cr ├── constraints │ ├── accept_constraint.cr │ ├── content_type_constraint.cr │ ├── format_constraint.cr │ ├── hash_constraint.cr │ ├── host_constraint.cr │ ├── methods_constraint.cr │ ├── params_constraint.cr │ ├── subdomain_constraint.cr │ └── web_socket_constraint.cr ├── controller.cr ├── controller │ ├── base.cr │ ├── cache_helpers.cr │ ├── rendering.cr │ ├── request_helpers.cr │ └── response_helpers.cr ├── dsl.cr ├── dsl │ ├── concerns.cr │ ├── constraints.cr │ ├── handlers.cr │ ├── helpers.cr │ ├── macros.cr │ ├── match.cr │ ├── mount.cr │ ├── request_methods.cr │ ├── resources.cr │ ├── root.cr │ ├── scope.cr │ ├── static.cr │ └── websockets.cr ├── error_page.html.ecr ├── errors.cr ├── exception_page.cr ├── handler.cr ├── handlers.cr ├── handlers │ ├── auto_close.cr │ ├── auto_mime.cr │ ├── config.cr │ ├── exceptions.cr │ ├── logger.cr │ ├── method_override_header.cr │ ├── method_override_param.cr │ ├── reset_path.cr │ ├── route_finder.cr │ └── scope_base_path.cr ├── helpers.cr ├── helpers │ └── mime_helper.cr ├── inflector │ ├── controllerize.cr │ ├── decontrollerize.cr │ ├── path_joiner.cr │ ├── pluralize.cr │ ├── random_const.cr │ ├── singularize.cr │ └── underscore.cr ├── pipeline.cr ├── router.cr ├── server.cr ├── server │ ├── context.cr │ ├── request.cr │ ├── request_processor.cr │ └── response.cr ├── view.cr ├── view │ ├── asset_tag_helpers.cr │ ├── cache_helpers.cr │ ├── capture_helper.cr │ ├── partial_helpers.cr │ ├── registry.cr │ ├── renderer.cr │ └── renderer │ │ ├── defs.cr │ │ ├── layout_finder.cr │ │ ├── partial_finder.cr │ │ ├── tokenize.cr │ │ └── view_finder.cr └── write_tracker.cr └── parse_version.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cr] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.github/workflows/crystal.yml: -------------------------------------------------------------------------------- 1 | name: Crystal CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: crystallang/crystal 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install dependencies 17 | run: shards install 18 | - name: Run tests 19 | run: crystal spec 20 | - name: Build Docs 21 | run: crystal docs 22 | - name: Deploy 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./docs 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /docs/ 3 | /lib/ 4 | /bin/ 5 | /.shards/ 6 | /benchmarks/*/.shards/ 7 | /benchmarks/*/shard.lock 8 | /benchmarks/*/Gemfile.lock 9 | /benchmarks/*/lib 10 | /benchmarks/*/bin 11 | /benchmarks/results.txt 12 | 13 | # Libraries don't need dependency lock 14 | # Dependencies will be locked in application that uses them 15 | /shard.lock 16 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | crystal 1.1.0 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jason@waldrip.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: 17 | - Subsystem: 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Obsidian Crystal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Orion](https://raw.githubusercontent.com/obsidian/orion/v3.0.0-dev/orion-banner.svg) 2 | 3 | [![Crystal CI](https://github.com/obsidian/orion/workflows/Crystal%20CI/badge.svg)](https://github.com/obsidian/orion/actions?query=workflow%3A%22Crystal+CI%22) 4 | [![GitHub issues](https://img.shields.io/github/issues/obsidian/orion)](https://github.com/obsidian/orion/issues) 5 | [![GitHub stars](https://img.shields.io/github/stars/obsidian/orion)](https://github.com/obsidian/orion/stargazers) 6 | [![GitHub license](https://img.shields.io/github/license/obsidian/orion)](https://github.com/obsidian/orion/blob/master/LICENSE) 7 | [![Documentation](https://img.shields.io/badge/Read-Documentation-%232E1052)](https://obsidian.github.io/orion) 8 | 9 | --- 10 | 11 | ## Introduction 12 | 13 | Orion is minimal, Omni-Conventional, declarative web framework inspired by the ruby-on-rails router and controller components. It provides, the routing, view, and controller framework of your application in a way that can be as simple or complex as you need it to fit your use case. 14 | 15 | ## Simple Example 16 | Orion out of the box is designed to be as simple as you want it to be. A few 17 | lines will get you a functioning web app. Orion also ships with helpful features 18 | such as view rendering and static content delivery. 19 | 20 | ```crystal 21 | require "orion/app" 22 | 23 | root do 24 | "Welcome Home" 25 | end 26 | 27 | get "/posts" do 28 | "Many posts here!" 29 | end 30 | ``` 31 | 32 | ## Flexible Routing 33 | Orion is extemely flexible, it is inspiried by the rails routing and controller framework and therefore has support for `scope`, `concerns`, `use HTTP::Handler`, `constraints` and more! See the modules in `Orion::DSL` more more detail. 34 | 35 | ```crystal 36 | require "orion/app" 37 | require "auth_handlers" 38 | 39 | static "/", dir: "./assets" 40 | 41 | scope "/api" do 42 | use AuthHandlers::Token 43 | end 44 | 45 | use AuthHandlers::CookieSession 46 | 47 | scope constraint: UnauthenticatedUser do 48 | root do 49 | render "views/home.slim" 50 | end 51 | 52 | get "/login", helper: login do 53 | render "views/login.slim" 54 | end 55 | 56 | post "/login" do 57 | if User.authenticate(params["email"], params["password"]) 58 | redirect to: root_path 59 | else 60 | flash[:error] = "Invalid login" 61 | redirect to: login_path 62 | end 63 | end 64 | 65 | scope constraint: AuthenticatedUser do 66 | root do 67 | render "views/dashboard.slim" 68 | end 69 | end 70 | ``` 71 | 72 | ## Installation 73 | Add this to your application's shard.yml: 74 | 75 | ```yml 76 | dependencies: 77 | orion: 78 | github: obsidian/orion 79 | ``` 80 | 81 | See also [Getting Started](https://github.com/obsidian/orion/wiki/Getting-Started). 82 | 83 | ## Documentation 84 | 85 | View the docs at [https://obsidian.github.io/orion](https://obsidian.github.io/orion). 86 | View the guides at [https://github.com/obsidian/orion/wiki](https://github.com/obsidian/orion/wiki). 87 | -------------------------------------------------------------------------------- /examples/base.cr: -------------------------------------------------------------------------------- 1 | require "../src/app" 2 | 3 | root do 4 | raise "Oops" 5 | end 6 | 7 | scope "/foo" do 8 | scope "/bar" do 9 | root do 10 | "Hello Foo" 11 | end 12 | end 13 | end 14 | 15 | get "/users", helper: "users" do 16 | render text: users_path 17 | end 18 | -------------------------------------------------------------------------------- /examples/simple-server.cr: -------------------------------------------------------------------------------- 1 | require "../src/orion" 2 | 3 | router MyApplication do 4 | use HTTP::LogHandler.new 5 | 6 | get "empty" do |context| 7 | context.response.puts "e" 8 | end 9 | 10 | get "/*", ->(context : Context) do 11 | context.response.puts "reviews" 12 | end 13 | 14 | get "/resources/js/*", ->(context : Context) do 15 | context.response.puts "somejs" 16 | end 17 | 18 | get "/robots.txt", ->(context : Context) do 19 | context.response.puts "robots" 20 | end 21 | end 22 | 23 | MyApplication.start(workers: System.cpu_count) 24 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: orion 2 | crystal: "~> 1.0" 3 | version: 4.0.0-beta4 4 | homepage: https://obsidian.github.io/orion 5 | documentation: https://obsidian.github.io/orion 6 | license: MIT 7 | 8 | authors: 9 | - Jason Waldrip 10 | 11 | dependencies: 12 | oak: 13 | github: obsidian/oak 14 | version: ">= 4.0.1" 15 | inflector: 16 | github: phoffer/inflector.cr 17 | kilt: 18 | github: jeromegn/kilt 19 | exception_page: 20 | github: crystal-loot/exception_page 21 | html_builder: 22 | github: crystal-lang/html_builder 23 | cache: 24 | github: mamantoha/cache 25 | crystar: 26 | github: naqvis/crystar 27 | -------------------------------------------------------------------------------- /spec/fixtures/index.html: -------------------------------------------------------------------------------- 1 | Index Page -------------------------------------------------------------------------------- /spec/fixtures/test.txt: -------------------------------------------------------------------------------- 1 | Hello World -------------------------------------------------------------------------------- /spec/orion/dsl/concerns_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::ConcernsSpec 4 | router SampleRouter do 5 | concern :messagable do 6 | get "messages/new", ->(c : Context) { c.response.print "lets send a message" } 7 | end 8 | 9 | scope "users" do 10 | implements :messagable 11 | end 12 | 13 | scope "groups" do 14 | implements :messagable 15 | end 16 | end 17 | 18 | describe "concerns" do 19 | it "should be present when included" do 20 | response = test_route(SampleRouter.new, :get, "/users/messages/new") 21 | response.status_code.should eq 200 22 | response.body.should eq "lets send a message" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/orion/dsl/constraints_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::ConstraintsSpec 4 | class TestConstraint 5 | include Orion::Constraint 6 | 7 | def matches?(request : ::HTTP::Request) 8 | request.headers["TEST"]? == "true" 9 | end 10 | end 11 | 12 | router SampleRouter do 13 | get "resources/:id", ->(c : Context) { c.response.print "resource #{c.request.path_params["id"]}" }, constraints: {id: /\d{4}/} 14 | get "alpha", ->(c : Context) { c.response.print "is js" }, format: "js" 15 | get "bravo", ->(c : Context) { c.response.print "is js or jsx" }, format: /jsx?/ 16 | get "charlie", ->(c : Context) { c.response.print "is an image" }, accept: "image/*" 17 | get "delta", ->(c : Context) { c.response.print "is a png image" }, accept: "image/png" 18 | post "mary", ->(c : Context) { c.response.print "is a jpg image" }, content_type: "image/jpeg" 19 | get "echo", ->(c : Context) { c.response.print "is a png image with unicode" }, accept: "image/png; charset=utf-8" 20 | 21 | host "example.org" do 22 | get "golf", ->(c : Context) { c.response.print "at host" } 23 | end 24 | 25 | subdomain "example" do 26 | get "hotel", ->(c : Context) { c.response.print "at subdomain" } 27 | end 28 | 29 | constraint TestConstraint.new do 30 | get "lima", ->(c : Context) { c.response.print "matches custom" } 31 | end 32 | 33 | constraints TestConstraint.new do 34 | get "zulu", ->(c : Context) { c.response.print "matches customs" } 35 | end 36 | end 37 | 38 | describe "constraints" do 39 | describe "for params" do 40 | context "if matched" do 41 | it "should pass" do 42 | response = test_route(SampleRouter.new, :get, "/resources/9999") 43 | response.status_code.should eq 200 44 | response.body.should eq "resource 9999" 45 | end 46 | end 47 | 48 | context "if not matched" do 49 | it "should not pass" do 50 | response = test_route(SampleRouter.new, :get, "/resources/123") 51 | response.status_code.should eq 404 52 | end 53 | end 54 | end 55 | 56 | describe "checking format" do 57 | context "with a string" do 58 | context "if matched" do 59 | it "should pass" do 60 | response = test_route(SampleRouter.new, :get, "/alpha.js") 61 | response.status_code.should eq 200 62 | response.body.should eq "is js" 63 | end 64 | end 65 | 66 | context "if not matched" do 67 | it "should not pass" do 68 | response = test_route(SampleRouter.new, :get, "/alpha.cr") 69 | response.status_code.should eq 404 70 | end 71 | end 72 | end 73 | end 74 | 75 | describe "checking content_type" do 76 | context "with a string" do 77 | context "if matched" do 78 | it "should pass" do 79 | response = test_route(SampleRouter.new, :post, "/mary", headers: {"Content-Type" => "image/jpeg"}, body: "aaa") 80 | response.status_code.should eq 200 81 | response.body.should eq "is a jpg image" 82 | end 83 | end 84 | 85 | context "if not matched without a body" do 86 | it "should pass" do 87 | response = test_route(SampleRouter.new, :post, "/mary") 88 | response.status_code.should eq 200 89 | response.body.should eq "is a jpg image" 90 | end 91 | end 92 | 93 | context "if not matched" do 94 | it "should not pass" do 95 | response = test_route(SampleRouter.new, :post, "/mary", body: "aaa") 96 | response.status_code.should eq 404 97 | end 98 | end 99 | end 100 | end 101 | 102 | describe "checking accept" do 103 | context "with a string" do 104 | context "if matched" do 105 | it "should pass" do 106 | response = test_route(SampleRouter.new, :get, "/delta", headers: {"Accept" => "image/png"}) 107 | response.status_code.should eq 200 108 | response.body.should eq "is a png image" 109 | end 110 | end 111 | 112 | context "if matched by extension" do 113 | it "should pass" do 114 | response = test_route(SampleRouter.new, :get, "/delta.png") 115 | response.status_code.should eq 200 116 | response.body.should eq "is a png image" 117 | end 118 | end 119 | 120 | context "if matched by wildcard" do 121 | it "should pass" do 122 | response = test_route(SampleRouter.new, :get, "/delta", headers: {"Accept" => "*/*"}) 123 | response.status_code.should eq 200 124 | response.body.should eq "is a png image" 125 | end 126 | end 127 | 128 | context "if not matched" do 129 | it "should not pass" do 130 | response = test_route(SampleRouter.new, :get, "/delta", headers: {"Accept" => "text/html"}) 131 | response.status_code.should eq 404 132 | end 133 | end 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/orion/dsl/handlers_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::HandlersSpec 4 | class AppendHandler 5 | include HTTP::Handler 6 | 7 | def initialize(@string : String) 8 | end 9 | 10 | def call(c : ::HTTP::Server::Context) 11 | call_next c 12 | c.response.print @string 13 | end 14 | end 15 | 16 | router SampleRouter do 17 | # use HTTP::ErrorHandler 18 | use AppendHandler.new ", and I am a guardian" 19 | root ->(c : Context) { c.response.print "I am Groot" } 20 | scope "scoped" do 21 | use AppendHandler.new ", and I am NOT a racoon" 22 | root ->(c : Context) { c.response.print "My name is Rocket" } 23 | end 24 | end 25 | 26 | describe "handlers" do 27 | it "should run root middleware" do 28 | response = test_route(SampleRouter.new, :get, "/") 29 | response.status_code.should eq 200 30 | response.body.should eq "I am Groot, and I am a guardian" 31 | end 32 | 33 | it "should run group middleware" do 34 | response = test_route(SampleRouter.new, :get, "/scoped") 35 | response.status_code.should eq 200 36 | response.body.should eq "My name is Rocket, and I am NOT a racoon, and I am a guardian" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/orion/dsl/helpers_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::HelpersSpec 4 | c = ->(c : HTTP::Server::Context) {} 5 | 6 | router SampleRouter do 7 | get "foo", c, helper: "foo" 8 | get "bars/:bar_id/locations/:location_id", c, helper: "bar" 9 | scope helper_prefix: "scoped" do 10 | get "baz", c, helper: "baz" 11 | post "bazs", c, helper: {name: "baz", prefix: "create"} 12 | get "bazs", c, helper: {name: "baz", suffix: "index"} 13 | end 14 | end 15 | 16 | describe "helpers" do 17 | it "should define a basic helper" do 18 | SampleRouter::RouteHelpers.foo_path.should eq "/foo" 19 | end 20 | 21 | it "should append params" do 22 | SampleRouter::RouteHelpers.foo_path(f: 1, b: "2", r: true).should eq "/foo?f=1&b=2&r=true" 23 | end 24 | 25 | it "should insert url params" do 26 | SampleRouter::RouteHelpers.bar_path(bar_id: 1, location_id: 5).should eq "/bars/1/locations/5" 27 | end 28 | 29 | it "should insert url params and append the rest" do 30 | SampleRouter::RouteHelpers.bar_path(bar_id: 1, location_id: 5, pour: true).should eq "/bars/1/locations/5?pour=true" 31 | end 32 | 33 | it "should raise if a param is missing" do 34 | expect_raises Orion::ParametersMissing do 35 | SampleRouter::RouteHelpers.bar_path(bar_id: 1) 36 | end 37 | end 38 | 39 | context "within scope" do 40 | it "should scope nested routes" do 41 | SampleRouter::RouteHelpers.scoped_baz_path.should eq "/baz" 42 | end 43 | 44 | it "should prefix a route" do 45 | SampleRouter::RouteHelpers.create_scoped_baz_path.should eq "/bazs" 46 | end 47 | 48 | it "should suffix a route" do 49 | SampleRouter::RouteHelpers.scoped_baz_index_path.should eq "/bazs" 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/orion/dsl/match_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::MatchSpec 4 | router SampleRouter do 5 | match "/callable", ->(c : Context) { c.response.print "callable match" } 6 | match "/block" do |c| 7 | c.response.print "block match" 8 | end 9 | match "/string" do |c| 10 | "im a string" 11 | end 12 | match "/to-match", to: "samples#to_match" 13 | match "/match-action", controller: SamplesController, action: action_match, helper: "sample_verbose" 14 | end 15 | 16 | class SamplesController < SampleRouter::BaseController 17 | def to_match 18 | response.print "to match" 19 | end 20 | 21 | def match 22 | response.print "controller match" 23 | end 24 | 25 | def action_match 26 | response.print "action match" 27 | end 28 | end 29 | 30 | {% for method in ::Orion::DSL::RequestMethods::METHODS %} 31 | describe {{ method.downcase }} do 32 | context "with callable" do 33 | it "should succeed" do 34 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/callable") 35 | response.status_code.should eq 200 36 | response.body.should eq "callable match" 37 | end 38 | end 39 | 40 | context "with a block" do 41 | it "should succeed" do 42 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/block") 43 | response.status_code.should eq 200 44 | response.body.should eq "block match" 45 | end 46 | end 47 | 48 | context "with a string return" do 49 | it "should succeed" do 50 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/string") 51 | response.status_code.should eq 200 52 | response.body.should eq "im a string\n" 53 | end 54 | end 55 | 56 | context "with to" do 57 | it "should succeed" do 58 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/to-match") 59 | response.status_code.should eq 200 60 | response.body.should eq "to match" 61 | end 62 | end 63 | 64 | context "with controller and action" do 65 | it "should succeed" do 66 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/match-action") 67 | response.status_code.should eq 200 68 | response.body.should eq "action match" 69 | end 70 | end 71 | end 72 | {% end %} 73 | end 74 | -------------------------------------------------------------------------------- /spec/orion/dsl/methods_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::MethodsSpec 4 | {% for method in ::Orion::DSL::RequestMethods::METHODS %} 5 | module {{ method.capitalize.id }} 6 | router SampleRouter do 7 | {{ method.downcase.id }} "/callable", ->(c : Context){ c.response.print "callable {{ method.downcase.id }}" } 8 | {{ method.downcase.id }} "/block", helper: "block" do |c| 9 | c.response.print "block {{ method.downcase.id }}" 10 | end 11 | {{ method.downcase.id }} "/to-{{ method.downcase.id }}", to: "samples#to_{{ method.downcase.id }}" 12 | {{ method.downcase.id }} "/{{ method.downcase.id }}-action", controller: SamplesController, action: action_{{ method.downcase.id }}, helper: "sample_verbose" 13 | end 14 | 15 | class SamplesController < SampleRouter::BaseController 16 | def to_{{ method.downcase.id }} 17 | response.print "to {{ method.downcase.id }}" 18 | end 19 | 20 | def {{ method.downcase.id }} 21 | response.print "controller {{ method.downcase.id }}" 22 | end 23 | 24 | def action_{{ method.downcase.id }} 25 | response.print "action {{ method.downcase.id }}" 26 | end 27 | end 28 | 29 | describe {{ method.downcase }} do 30 | context "with callable" do 31 | it "should succeed" do 32 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/callable") 33 | response.status_code.should eq 200 34 | response.body.should eq "callable {{ method.downcase.id }}" 35 | end 36 | end 37 | 38 | context "with a block" do 39 | it "should succeed" do 40 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/block") 41 | response.status_code.should eq 200 42 | response.body.should eq "block {{ method.downcase.id }}" 43 | end 44 | end 45 | 46 | context "with to" do 47 | it "should succeed" do 48 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/to-{{ method.downcase.id }}") 49 | response.status_code.should eq 200 50 | response.body.should eq "to {{ method.downcase.id }}" 51 | end 52 | end 53 | 54 | context "with controller and action" do 55 | it "should succeed" do 56 | response = test_route(SampleRouter.new, :{{ method.downcase.id }}, "/{{ method.downcase.id }}-action") 57 | response.status_code.should eq 200 58 | response.body.should eq "action {{ method.downcase.id }}" 59 | end 60 | end 61 | end 62 | end 63 | {% end %} 64 | end 65 | -------------------------------------------------------------------------------- /spec/orion/dsl/resources_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::Resources::Spec 4 | router SampleRouter do 5 | resources :users do 6 | get "profile", action: profile 7 | end 8 | 9 | resources :users_constrained, controller: UsersController, id_constraint: /^\d{4}$/, id_param: :user_id 10 | resources :users_api, controller: UsersController, id_param: :user_id, format: "json" 11 | resources :users_api_2, controller: UsersController, id_param: :user_id, accept: "application/json" 12 | 13 | resource :person do 14 | get "profile", action: profile 15 | end 16 | resource :person_api, controller: PersonController, format: "json" 17 | resource :person_api_2, controller: PersonController, accept: "application/json" 18 | end 19 | 20 | class UsersController < SampleRouter::BaseController 21 | def profile 22 | response.print "profile #{request.path_params["user_id"]}" 23 | end 24 | 25 | def index 26 | response.print "index" 27 | end 28 | 29 | def new 30 | response.print "new" 31 | end 32 | 33 | def create 34 | response.print "create" 35 | end 36 | 37 | def show 38 | response.print "show #{request.path_params["user_id"]}" 39 | end 40 | 41 | def edit 42 | response.print "edit #{request.path_params["user_id"]}" 43 | end 44 | 45 | def update 46 | response.print "update #{request.path_params["user_id"]}" 47 | end 48 | 49 | def delete 50 | response.print "delete #{request.path_params["user_id"]}" 51 | end 52 | end 53 | 54 | class PersonController < SampleRouter::BaseController 55 | def profile 56 | response.print "profile" 57 | end 58 | 59 | def new 60 | response.print "new" 61 | end 62 | 63 | def create 64 | response.print "create" 65 | end 66 | 67 | def show 68 | response.print "show" 69 | end 70 | 71 | def edit 72 | response.print "edit" 73 | end 74 | 75 | def update 76 | response.print "update" 77 | end 78 | 79 | def delete 80 | response.print "delete" 81 | end 82 | end 83 | 84 | describe ".resources" do 85 | it "should return the index action" do 86 | response = test_route(SampleRouter.new, :get, SampleRouter::RouteHelpers.users_path) 87 | response.status_code.should eq 200 88 | response.body.should eq "index" 89 | end 90 | 91 | it "should return the new action" do 92 | response = test_route(SampleRouter.new, :get, SampleRouter::RouteHelpers.new_user_path) 93 | response.status_code.should eq 200 94 | response.body.should eq "new" 95 | end 96 | 97 | it "should return the create action" do 98 | response = test_route(SampleRouter.new, :post, SampleRouter::RouteHelpers.users_path) 99 | response.status_code.should eq 200 100 | response.body.should eq "create" 101 | end 102 | 103 | it "should return the show action" do 104 | response = test_route(SampleRouter.new, :get, SampleRouter::RouteHelpers.user_path user_id: 1) 105 | response.status_code.should eq 200 106 | response.body.should eq "show 1" 107 | end 108 | 109 | it "should return the edit action" do 110 | response = test_route(SampleRouter.new, :get, SampleRouter::RouteHelpers.edit_user_path user_id: 1) 111 | response.status_code.should eq 200 112 | response.body.should eq "edit 1" 113 | end 114 | 115 | it "should return the update action" do 116 | response = test_route(SampleRouter.new, :put, SampleRouter::RouteHelpers.user_path user_id: 1) 117 | response.status_code.should eq 200 118 | response.body.should eq "update 1" 119 | end 120 | 121 | it "should return the update action" do 122 | response = test_route(SampleRouter.new, :patch, SampleRouter::RouteHelpers.user_path user_id: 1) 123 | response.status_code.should eq 200 124 | response.body.should eq "update 1" 125 | end 126 | 127 | it "should return the update action" do 128 | response = test_route(SampleRouter.new, :delete, SampleRouter::RouteHelpers.user_path user_id: 1) 129 | response.status_code.should eq 200 130 | response.body.should eq "delete 1" 131 | end 132 | 133 | it "should return the profile action" do 134 | response = test_route(SampleRouter.new, :get, "users/1/profile") 135 | response.status_code.should eq 200 136 | response.body.should eq "profile 1" 137 | end 138 | 139 | context "with an id constraint" do 140 | it "should return 200 when matched" do 141 | response = test_route(SampleRouter.new, :get, "users_constrained/9999") 142 | response.status_code.should eq 200 143 | response.body.should eq "show 9999" 144 | end 145 | 146 | it "should return 404 when not matched" do 147 | response = test_route(SampleRouter.new, :get, "users_constrained/9") 148 | response.status_code.should eq 404 149 | end 150 | end 151 | 152 | context "with a format constraint" do 153 | it "should return 200 when matched" do 154 | response = test_route(SampleRouter.new, :get, "users_api/1.json") 155 | response.status_code.should eq 200 156 | response.body.should eq "show 1" 157 | end 158 | 159 | it "should return 404 when not matched" do 160 | response = test_route(SampleRouter.new, :get, "users_api/1") 161 | response.status_code.should eq 404 162 | end 163 | end 164 | 165 | context "with an accept constraint" do 166 | it "should return 200 when matched with format" do 167 | response = test_route(SampleRouter.new, :get, "users_api_2/1.json") 168 | response.status_code.should eq 200 169 | response.body.should eq "show 1" 170 | end 171 | 172 | it "should return 200 when matched with header" do 173 | response = test_route(SampleRouter.new, :get, "users_api_2/1", headers: {"Accept" => "application/json"}) 174 | response.status_code.should eq 200 175 | response.body.should eq "show 1" 176 | end 177 | 178 | it "should return 404 when not matched" do 179 | response = test_route(SampleRouter.new, :get, "users_api_2/1", headers: {"Accept" => "text/html"}) 180 | response.status_code.should eq 404 181 | end 182 | end 183 | end 184 | 185 | describe ".resource" do 186 | it "should return the new action" do 187 | response = test_route(SampleRouter.new, :get, SampleRouter::RouteHelpers.new_person_path) 188 | response.status_code.should eq 200 189 | response.body.should eq "new" 190 | end 191 | 192 | it "should return the create action" do 193 | response = test_route(SampleRouter.new, :post, SampleRouter::RouteHelpers.person_path) 194 | response.status_code.should eq 200 195 | response.body.should eq "create" 196 | end 197 | 198 | it "should return the show action" do 199 | response = test_route(SampleRouter.new, :get, SampleRouter::RouteHelpers.person_path) 200 | response.status_code.should eq 200 201 | response.body.should eq "show" 202 | end 203 | 204 | it "should return the edit action" do 205 | response = test_route(SampleRouter.new, :get, SampleRouter::RouteHelpers.edit_person_path) 206 | response.status_code.should eq 200 207 | response.body.should eq "edit" 208 | end 209 | 210 | it "should return the update action" do 211 | response = test_route(SampleRouter.new, :put, SampleRouter::RouteHelpers.person_path) 212 | response.status_code.should eq 200 213 | response.body.should eq "update" 214 | end 215 | 216 | it "should return the update action" do 217 | response = test_route(SampleRouter.new, :patch, SampleRouter::RouteHelpers.person_path) 218 | response.status_code.should eq 200 219 | response.body.should eq "update" 220 | end 221 | 222 | it "should return the update action" do 223 | response = test_route(SampleRouter.new, :delete, SampleRouter::RouteHelpers.person_path) 224 | response.status_code.should eq 200 225 | response.body.should eq "delete" 226 | end 227 | 228 | it "should return the profile action" do 229 | response = test_route(SampleRouter.new, :get, "person/profile") 230 | response.status_code.should eq 200 231 | response.body.should eq "profile" 232 | end 233 | 234 | context "with a format constraint" do 235 | it "should return 200 when matched" do 236 | response = test_route(SampleRouter.new, :get, "person_api.json") 237 | response.status_code.should eq 200 238 | response.body.should eq "show" 239 | end 240 | 241 | it "should return 404 when not matched" do 242 | response = test_route(SampleRouter.new, :get, "person_api") 243 | response.status_code.should eq 404 244 | end 245 | end 246 | 247 | context "with an accept constraint" do 248 | it "should return 200 when matched with format" do 249 | response = test_route(SampleRouter.new, :get, "person_api_2.json") 250 | response.status_code.should eq 200 251 | response.body.should eq "show" 252 | end 253 | 254 | it "should return 200 when matched with header" do 255 | response = test_route(SampleRouter.new, :get, "person_api_2", headers: {"Accept" => "application/json"}) 256 | response.status_code.should eq 200 257 | response.body.should eq "show" 258 | end 259 | 260 | it "should return 404 when not matched" do 261 | response = test_route(SampleRouter.new, :get, "person_api_2", headers: {"Accept" => "text/html"}) 262 | response.status_code.should eq 404 263 | end 264 | end 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /spec/orion/dsl/scope_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::ScopeSpec 4 | router SampleRouter do 5 | get "home", ->(c : Context) { c.response.print c.request.base_path } 6 | scope "messages" do 7 | get "new", ->(c : Context) { c.response.print c.request.base_path } 8 | end 9 | end 10 | 11 | describe "scope" do 12 | context "out of scope" do 13 | it "should have the default base path" do 14 | response = test_route(SampleRouter.new, :get, "/home") 15 | response.status_code.should eq 200 16 | response.body.should eq "/" 17 | end 18 | end 19 | 20 | context "within scope" do 21 | it "should set the base path" do 22 | response = test_route(SampleRouter.new, :get, "/messages/new") 23 | response.status_code.should eq 200 24 | response.body.should eq "/messages" 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/orion/dsl/websockets_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Orion::DSL::WebSocketsSpec 4 | router SampleRouter do 5 | ws "/match", ->(ws : WebSocket, c : Context) { 6 | ws.send("Match") 7 | } 8 | get "/match", ->(c : Context) { 9 | c.response.print("Match Non WS") 10 | } 11 | end 12 | 13 | describe "ws" do 14 | it "matches on given route" do 15 | io, response = test_ws(SampleRouter.new, "/match") 16 | io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") 17 | end 18 | 19 | it "returns 404 for an unmatched route" do 20 | io, response = test_ws(SampleRouter.new, "/no_match") 21 | response.status_code.should eq(404) 22 | end 23 | 24 | it "should allow a non ws request to coexist" do 25 | response = test_route(SampleRouter.new, :get, "/match") 26 | response.status_code.should eq 200 27 | response.body.should eq "Match Non WS" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/orion/handlers/method_override_param_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Orion::Handlers::MethodOverrideParam do 4 | context "given a query param" do 5 | it "should set override the method" do 6 | context = mock_context(:get, "/?_method=POST") 7 | Orion::Handlers::MethodOverrideParam.new.call(context) 8 | context.request.method.should eq "POST" 9 | end 10 | end 11 | 12 | context "given a form param" do 13 | it "should set override the method" do 14 | io = IO::Memory.new 15 | builder = HTTP::FormData::Builder.new(io) 16 | builder.field("_method", "POST") 17 | builder.finish 18 | io.rewind 19 | context = mock_context(:get, "/", body: io, headers: {"Content-Type" => "multipart/form-data; boundary=\"#{builder.boundary}\""}) 20 | Orion::Handlers::MethodOverrideParam.new.call(context) 21 | context.request.method.should eq "POST" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/orion/router_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | params = {} of String => String 4 | 5 | module RouterSpec 6 | router Router do 7 | static path: "/assets", dir: "./spec/fixtures" 8 | root ->(c : Context) { c.response.print "I am Groot" } 9 | root to: "sample#action" 10 | root to: "Sample#action" 11 | root controller: SampleController, action: action 12 | root do |c| 13 | params = c.request.path_params 14 | end 15 | get "/:first/:second", ->(c : Context) { params = c.request.path_params } 16 | get "/:first/:second", to: "sample#action" 17 | get "/:first/:second", to: "Sample#action" 18 | get "/:first/:second", to: "Sample#action" 19 | get "/:first/:second", controller: SampleController, action: action 20 | get "/:first/:second" do |c| 21 | params = c.request.path_params 22 | end 23 | head "/:first/:second", ->(c : Context) { params = c.request.path_params } 24 | head "/:first/:second", to: "sample#action" 25 | head "/:first/:second", to: "Sample#action" 26 | head "/:first/:second", controller: SampleController, action: action 27 | head "/:first/:second" do |c| 28 | params = c.request.path_params 29 | end 30 | post "/:first/:second", ->(c : Context) { params = c.request.path_params } 31 | post "/:first/:second", to: "sample#action" 32 | post "/:first/:second", to: "Sample#action" 33 | post "/:first/:second", controller: SampleController, action: action 34 | post "/:first/:second" do |c| 35 | params = c.request.path_params 36 | end 37 | put "/:first/:second", ->(c : Context) { params = c.request.path_params } 38 | put "/:first/:second", to: "sample#action" 39 | put "/:first/:second", to: "Sample#action" 40 | put "/:first/:second", controller: SampleController, action: action 41 | put "/:first/:second" do |c| 42 | params = c.request.path_params 43 | end 44 | delete "/:first/:second", ->(c : Context) { params = c.request.path_params } 45 | delete "/:first/:second", to: "sample#action" 46 | delete "/:first/:second", to: "Sample#action" 47 | delete "/:first/:second", controller: SampleController, action: action 48 | delete "/:first/:second" do |c| 49 | params = c.request.path_params 50 | end 51 | connect "/:first/:second", ->(c : Context) { params = c.request.path_params } 52 | connect "/:first/:second", to: "sample#action" 53 | connect "/:first/:second", to: "Sample#action" 54 | connect "/:first/:second", controller: SampleController, action: action 55 | connect "/:first/:second" do |c| 56 | params = c.request.path_params 57 | end 58 | options "/:first/:second", ->(c : Context) { params = c.request.path_params } 59 | options "/:first/:second", to: "sample#action" 60 | options "/:first/:second", to: "Sample#action" 61 | options "/:first/:second", controller: SampleController, action: action 62 | options "/:first/:second" do |c| 63 | params = c.request.path_params 64 | end 65 | trace "/:first/:second", ->(c : Context) { params = c.request.path_params } 66 | trace "/:first/:second", to: "sample#action" 67 | trace "/:first/:second", to: "Sample#action" 68 | trace "/:first/:second", controller: SampleController, action: action 69 | trace "/:first/:second" do |c| 70 | params = c.request.path_params 71 | end 72 | patch "/:first/:second", ->(c : Context) { params = c.request.path_params } 73 | patch "/:first/:second", to: "sample#action" 74 | patch "/:first/:second", to: "Sample#action" 75 | patch "/:first/:second", controller: SampleController, action: action 76 | patch "/:first/:second" do |c| 77 | params = c.request.path_params 78 | end 79 | match "/:first/:second", ->(c : Context) { params = c.request.path_params } 80 | match "/:first/:second", to: "sample#action" 81 | match "/:first/:second", to: "Sample#action" 82 | match "/:first/:second", controller: SampleController, action: action 83 | match "/:first/:second" do |c| 84 | params = c.request.path_params 85 | end 86 | put "/hello", ->(c : Context) { c.response.print "I put things" } 87 | ws "/socket", ->(ws : WebSocket, c : Context) { 88 | ws.send "hello world" 89 | } 90 | ws "/:first/:second", to: "sample#action" 91 | ws "/:first/:second", to: "Sample#action" 92 | ws "/:first/:second", controller: SampleController, action: socket_action 93 | ws "/:first/:second" do |ws, req| 94 | ws.send "hello world" 95 | end 96 | end 97 | 98 | class SampleController < Router::BaseController 99 | def action 100 | params = request.path_params 101 | end 102 | 103 | def socket_action 104 | websocket.send "Hello World" 105 | end 106 | end 107 | 108 | describe "a basic router" do 109 | it "should run a basic route" do 110 | response = test_route(Router.new, :get, Router::RouteHelpers.root_path) 111 | response.status_code.should eq 200 112 | response.body.should eq "I am Groot" 113 | end 114 | 115 | it "should parse params" do 116 | response = test_route(Router.new, :get, "/foo/bar") 117 | response.status_code.should eq 200 118 | params["first"].should eq "foo" 119 | params["second"].should eq "bar" 120 | end 121 | end 122 | 123 | describe "method override header" do 124 | it "should override the header" do 125 | response = test_route(Router.new, :get, "/hello", headers: {"X-Method-Override" => "PUT"}) 126 | response.status_code.should eq 200 127 | response.body.should eq "I put things" 128 | end 129 | end 130 | 131 | describe "missing route" do 132 | it "should return 404" do 133 | response = test_route(Router.new, :get, "/missing") 134 | response.status_code.should eq 404 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "../src/orion" 2 | require "spec" 3 | 4 | def mock_context(method, path, host = "example.org", *, headers = {} of String => String, io = IO::Memory.new, body = nil) 5 | http_headers = HTTP::Headers.new 6 | headers.each { |k, v| http_headers[k] = v } 7 | http_headers["HOST"] = host 8 | request = Orion::Server::Request.new(method.to_s.upcase, path, http_headers) 9 | request.body = body 10 | response = Orion::Server::Response.new io 11 | Orion::Server::Context.new(request, response) 12 | end 13 | 14 | def test_route(router : Orion::Router, method, path, *, headers = {} of String => String, body = nil) 15 | io = IO::Memory.new 16 | context = mock_context(method, path, headers: headers, io: io, body: body) 17 | router.call(context) 18 | HTTP::Client::Response.from_io io.tap(&.rewind) 19 | end 20 | 21 | def test_ws(router : Orion::Router, path, host = "example.org") 22 | input_io = IO::Memory.new 23 | output_io = IO::Memory.new 24 | context = mock_context("GET", path, host, headers: { 25 | "Upgrade" => "websocket", 26 | "Connection" => "Upgrade", 27 | "Sec-WebSocket-Key" => "dGhlIHNhbXBsZSBub25jZQ==", 28 | "Sec-WebSocket-Version" => "13", 29 | }, io: output_io) 30 | context.request.to_io(input_io) 31 | begin 32 | router.processor.process(input_io.tap(&.rewind), output_io) 33 | rescue IO::Error 34 | # Raises because the IO:: Memory is empty 35 | end 36 | response = HTTP::Client::Response.from_io output_io.tap(&.rewind) 37 | {output_io, response} 38 | end 39 | -------------------------------------------------------------------------------- /src/app.cr: -------------------------------------------------------------------------------- 1 | require "./orion" 2 | include Orion::DSL 3 | -------------------------------------------------------------------------------- /src/http.cr: -------------------------------------------------------------------------------- 1 | require "http" 2 | require "./http/*" 3 | -------------------------------------------------------------------------------- /src/http/request.cr: -------------------------------------------------------------------------------- 1 | class HTTP::Request 2 | # :nodoc: 3 | setter path_params : Hash(String, String)? 4 | property base_path : String = "/" 5 | property action : Orion::Action? 6 | 7 | # Returns the list of path params set by an Orion route. 8 | def path_params 9 | @path_params ||= {} of String => String 10 | end 11 | 12 | # The format of the http request 13 | def format 14 | formats.first 15 | end 16 | 17 | # The formats of the http request 18 | def formats 19 | Orion::MIMEHelper.request_extensions(self).tap do |set| 20 | set << File.extname(resource) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/macro.cr: -------------------------------------------------------------------------------- 1 | # Define a new router 2 | macro router(name) 3 | module {{ name }} 4 | include Orion::DSL 5 | 6 | {{ yield }} 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/orion.cr: -------------------------------------------------------------------------------- 1 | require "oak" 2 | require "kilt" 3 | require "./http" 4 | require "./macro" 5 | require "./orion/*" 6 | 7 | module Orion 8 | # :nodoc: 9 | FLAGS = {} of String => Bool 10 | 11 | alias Logger = Handlers::Logger 12 | 13 | {{ run "./parse_version.cr" }} 14 | end 15 | -------------------------------------------------------------------------------- /src/orion/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.0.0 2 | 3 | ## New Features 4 | 5 | * A new view system allows for users to render views and partials while maintaining a layer of security between the view and controller. 6 | * View helpers allow you do define helpers via a module or block to extend methods into your view layer. 7 | * Built in view helpers. 8 | * Statically served assets created with the `static` macro are now bundled with the binary in release mode and are unpacked when the server starts. 9 | 10 | 11 | ## Breaking Changes 12 | 13 | * Views are no longer rendered inline within the controller. Local variable access and instance variables must be passed as a named tuple to the `locals` key on the render method and accessed via `locals[:foo]` within the view. 14 | 15 | ## Bug Fixes 16 | 17 | * Servers created with `require orion/app` were not getting their entire user defined config before booting up. This is now fixed. 18 | 19 | # 3.1.0 20 | 21 | 22 | 23 | ## New Features 24 | 25 | ## Breaking Changes 26 | 27 | ## Bug Fixes 28 | 29 | # 3.0.0 30 | 31 | ## New Features 32 | 33 | ## Breaking Changes 34 | 35 | ## Bug Fixes -------------------------------------------------------------------------------- /src/orion/action.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::Action 3 | getter helper : String? 4 | getter constraints = [] of Constraint 5 | @proc : Handler::HandlerProc 6 | @pipeline : Orion::Pipeline 7 | 8 | def initialize(@proc : Handler::HandlerProc, *, handlers = [] of ::HTTP::Handler, constraints = [] of Constraint, @helper = nil) 9 | @constraints = constraints.dup 10 | @pipeline = Pipeline.new(handlers) 11 | end 12 | 13 | def invoke(c) 14 | @proc.call(c) 15 | end 16 | 17 | def call(c) 18 | c.request.action = self 19 | @pipeline.call(c) 20 | end 21 | 22 | def matches_constraints?(request : ::HTTP::Request) 23 | constraints.all? &.matches?(request) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/orion/assets.cr: -------------------------------------------------------------------------------- 1 | require "uuid" 2 | 3 | module Assets 4 | def self.unpack(pack) 5 | File.join(Dir.tempdir, "orion", "pack", UUID.random.to_s).tap do |dir| 6 | io = IO::Memory.new 7 | Base64.decode(pack, io) 8 | Compress::Gzip::Reader.open(io.rewind) do |gzip| 9 | Crystar::Reader.open(gzip) do |tar| 10 | tar.each_entry do |entry| 11 | filename = File.join(dir, entry.name) 12 | Dir.mkdir_p(File.dirname(filename)) 13 | File.open filename, "w+" do |file| 14 | IO.copy entry.io, file 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/orion/assets/pack.cr: -------------------------------------------------------------------------------- 1 | require "crystar" 2 | require "compress/gzip" 3 | dir = ARGV[0] 4 | 5 | io = IO::Memory.new 6 | Compress::Gzip::Writer.open(io, level: Compress::Gzip::BEST_COMPRESSION) do |gzip| 7 | Crystar::Writer.open(gzip) do |tar| 8 | Dir.glob(File.join(dir, "**", "*"), match_hidden: true).each do |filename| 9 | File.open(filename) do |file| 10 | hdr = Crystar.file_info_header(file, file.path) 11 | hdr.name = filename 12 | tar.write_header hdr 13 | tar.write file.gets_to_end.to_slice 14 | end unless File.directory? filename 15 | end 16 | end 17 | end 18 | Base64.encode(io).split("\n").join.inspect(STDOUT) 19 | -------------------------------------------------------------------------------- /src/orion/cache.cr: -------------------------------------------------------------------------------- 1 | require "cache" 2 | 3 | class Orion::Cache 4 | @store : ::Cache::Store(String, String) 5 | 6 | def initialize(@store = ::Cache::NullStore(String, String).new(expires_in: 0.seconds)) 7 | end 8 | 9 | delegate read, write, fetch, delete, clear, to: @store 10 | 11 | # Read an item from cache 12 | def read(keyable : Keyable) 13 | read(keyable.cache_key) 14 | end 15 | 16 | # Write an item to cache 17 | def write(keyable : Keyable, value) 18 | write(keyable.cache_key, value) 19 | end 20 | 21 | # Read the item from cache, if it doesn't exist, invoke the block 22 | def fetch(key : Keyable, &block) 23 | fetch(keyable.cache_key, &block) 24 | end 25 | 26 | # If the conition is true invoke `fetch` 27 | def fetch_if(condition, key, &block) 28 | condition ? fetch(key, &block) : yield 29 | end 30 | 31 | # Delete the item from cache 32 | def delete(keyable : Keyable, value) 33 | delete(keyable.cache_key, value) 34 | end 35 | end 36 | 37 | require "./cache/*" 38 | -------------------------------------------------------------------------------- /src/orion/cache/keyable.cr: -------------------------------------------------------------------------------- 1 | module Orion::Cache::Keyable 2 | macro define_cache_key(*keys) 3 | def cache_key : String 4 | {{ keys.map(&.id) }}.join("-") 5 | end 6 | end 7 | 8 | abstract def cache_key : String 9 | end 10 | -------------------------------------------------------------------------------- /src/orion/config.cr: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "openssl" 3 | 4 | # These are the options available when setting properties with the `config` 5 | # method within your application. 6 | class Orion::Config 7 | struct ReadOnly 8 | getter port : Int32? 9 | getter address : ::Socket::IPAddress | ::Socket::UNIXAddress | Nil 10 | getter host : String? 11 | getter path : String? 12 | getter name : String 13 | getter uri : URI? 14 | getter workers : Int32 | Int64 15 | getter asset_host : String? 16 | getter cache : Orion::Cache 17 | getter logger : Log? 18 | 19 | def initialize(config : Orion::Config) 20 | @port = config.port 21 | @address = config.address 22 | @host = config.host 23 | @path = config.path 24 | @name = config.name 25 | @uri = config.uri 26 | @workers = config.workers 27 | @asset_host = config.asset_host 28 | @cache = config.cache 29 | @logger = config.logger 30 | end 31 | end 32 | 33 | setter port : Int32? = 4000 34 | setter address : ::Socket::IPAddress | ::Socket::UNIXAddress | Nil 35 | setter host : String = ::Socket::IPAddress::LOOPBACK 36 | setter path : String? 37 | 38 | property name : String = File.basename Dir.current 39 | property socket : ::Socket::Server? 40 | property tls : ::OpenSSL::SSL::Context::Server? 41 | property reuse_port : Bool = false 42 | property autoclose : Bool = true 43 | property strip_extension : Bool = false 44 | property workers : Int32 | Int64 = 1 45 | property asset_host : String? = nil 46 | property cache : Orion::Cache = Orion::Cache.new 47 | property logger : Log? = Log.for(Orion) 48 | 49 | def port=(port : String) 50 | self.port = port.to_i32 51 | end 52 | 53 | def port=(port : Nil) 54 | end 55 | 56 | def uri=(uri : String) 57 | self.uri = URI.parse(uri) 58 | end 59 | 60 | def uri=(uri : URI) 61 | case uri.scheme 62 | when "tcp" 63 | self.address = Socket::IPAddress.parse(uri) 64 | when "unix" 65 | self.address = Socket::UNIXAddress.parse(uri) 66 | when "tls" 67 | self.address = Socket::IPAddress.parse(uri) 68 | self.tls = OpenSSL::SSL::Context::Server.from_hash(HTTP::Params.parse(uri.query || "")) 69 | else 70 | raise ArgumentError.new "Unsupported socket type: #{uri.scheme}" 71 | end 72 | end 73 | 74 | def uri 75 | case {address = self.address, tls = self.tls} 76 | when {::Socket::IPAddress, ::OpenSSL::SSL::Context::Server} 77 | URI.new(scheme: "tls", host: address.address, port: address.port) 78 | when {::Socket::IPAddress, Nil} 79 | URI.new(scheme: "tcp", host: address.address, port: address.port) 80 | when {::Socket::UNIXAddress, _} 81 | URI.new(scheme: "unix", path: address.path) 82 | else 83 | nil 84 | end 85 | end 86 | 87 | def host 88 | case (address = @address) 89 | when ::Socket::IPAddress 90 | address.address 91 | when ::Socket::UNIXAddress 92 | nil 93 | else 94 | @host 95 | end 96 | end 97 | 98 | def port 99 | case (address = @address) 100 | when ::Socket::IPAddress 101 | address.port 102 | when ::Socket::UNIXAddress 103 | nil 104 | else 105 | @port 106 | end 107 | end 108 | 109 | def path 110 | case (address = @address) 111 | when ::Socket::UNIXAddress 112 | address.path 113 | when ::Socket::IPAddress 114 | nil 115 | else 116 | @path 117 | end 118 | end 119 | 120 | def address 121 | address = @address 122 | host = @host 123 | port = @port 124 | path = @path 125 | return address if address 126 | return ::Socket::IPAddress.new(host, port) if host && port 127 | return ::Socket::UNIXAddress.new(path) if path 128 | end 129 | 130 | def readonly 131 | ReadOnly.new self 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /src/orion/constraint.cr: -------------------------------------------------------------------------------- 1 | # Include `Orion::Constraint` module and implment it's required methods to 2 | # create custom constraints. You can read more about constraints in 3 | # `Orion::DSL::Constraints`. 4 | module Orion::Constraint 5 | abstract def matches?(request : HTTP::Request) 6 | end 7 | 8 | require "./constraints/*" 9 | -------------------------------------------------------------------------------- /src/orion/constraints/accept_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::AcceptConstraint 3 | include Constraint 4 | 5 | def initialize(@accept : String | Array(String)) 6 | end 7 | 8 | def matches?(request : ::HTTP::Request) 9 | return false unless request.headers["Accept"]? 10 | type_for_accept(request).any? do |mime_type| 11 | matches?(mime_type, @accept) 12 | end 13 | end 14 | 15 | private def matches?(mime_type : String, string : String) 16 | mime_parts = mime_type.split("/") 17 | match_parts = string.split("/") 18 | category_matches = mime_parts[0] === match_parts[0] || mime_parts[0] === "*" || match_parts[0] === "*" 19 | format_matches = mime_parts[1] === match_parts[1] || mime_parts[1] === "*" || match_parts[1] === "*" 20 | category_matches && format_matches 21 | end 22 | 23 | private def matches?(mime_type : String, strings : Array(String)) 24 | strings.any? do |string| 25 | matches?(mime_type, string) 26 | end 27 | end 28 | 29 | private def type_for_accept(request : HTTP::Request) 30 | request.headers["accept"]?.to_s.split(",").map(&.strip).map do |accept| 31 | accept.split(";").map(&.strip) 32 | end.sort_by do |parts| 33 | part = parts[1..-1].find { |p| p.starts_with? "q=" } 34 | part ? -part.split("=")[-1].strip.to_f : -1 35 | end.map(&.[0]).map do |content_type| 36 | content_type 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /src/orion/constraints/content_type_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::ContentTypeConstraint 3 | include Constraint 4 | 5 | def initialize(@content_type : String | Array(String)) 6 | end 7 | 8 | def matches?(request : ::HTTP::Request) 9 | return true unless request.body 10 | return false unless request.headers["Content-Type"]? 11 | matches?(type_for_request(request), @content_type) 12 | end 13 | 14 | private def matches?(mime_type : String, string : String) 15 | mime_type == string 16 | end 17 | 18 | private def matches?(mime_type : String, strings : Array(String)) 19 | strings.any? do |string| 20 | matches?(mime_type, string) 21 | end 22 | end 23 | 24 | def type_for_request(request : HTTP::Request) 25 | content_type = request.headers["content-type"]?.to_s.split(';').first 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/orion/constraints/format_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::FormatConstraint 3 | include Constraint 4 | 5 | def initialize(@format : String | Regex | Array(String)) 6 | end 7 | 8 | def matches?(request : ::HTTP::Request) 9 | extension = File.extname(request.path).lchop(".") 10 | matches?(extension, @format) 11 | end 12 | 13 | private def matches?(extension : String, string : String) 14 | extension == string.lchop(".") 15 | end 16 | 17 | private def matches?(extension : String, regex : Regex) 18 | extension =~ regex 19 | end 20 | 21 | private def matches?(extension : String, strings : Array(String)) 22 | strings.any? do |string| 23 | matches?(extension, string) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/orion/constraints/hash_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Orion::HashConstraint(T) 3 | include Constraint 4 | 5 | def initialize(constraints : Hash(Symbol, T)) 6 | hash = constraints.each_with_object({} of String => T) do |(key, value), hash| 7 | hash[key.to_s] = value 8 | end 9 | initialize(hash) 10 | end 11 | 12 | def initialize(constraints : Hash(String, T)) 13 | @constraints = constraints 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/orion/constraints/host_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::HostConstraint 3 | include Constraint 4 | 5 | def initialize(@constraint : String | Regex) 6 | end 7 | 8 | def matches?(request : ::HTTP::Request) 9 | if host = request.hostname 10 | matches? host, @constraint 11 | end 12 | end 13 | 14 | private def matches?(host : String, string : String) 15 | host == string 16 | end 17 | 18 | private def matches?(host : String, regex : Regex) 19 | host =~ regex 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/orion/constraints/methods_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::RequestMethodsConstraint 3 | include Constraint 4 | 5 | @methods : Array(String) 6 | 7 | def initialize(method : String) 8 | initialize([method]) 9 | end 10 | 11 | def initialize(methods : Array(String)) 12 | @methods = methods.map(&.downcase) 13 | end 14 | 15 | def matches?(request : ::HTTP::Request) 16 | return true if request.method.downcase == "*" 17 | @methods.any?(&.== request.method.downcase) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/orion/constraints/params_constraint.cr: -------------------------------------------------------------------------------- 1 | require "./hash_constraint" 2 | 3 | # :nodoc: 4 | struct Orion::ParamsConstraint 5 | include HashConstraint(Regex) 6 | 7 | def matches?(request : ::HTTP::Request) 8 | @constraints.all? do |key, regex| 9 | next true if key.empty? 10 | if value = request.path_params[key]? 11 | regex.match value 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/orion/constraints/subdomain_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::SubdomainConstraint 3 | include Constraint 4 | 5 | def initialize(constraints) 6 | initialize constraints.to_hash 7 | end 8 | 9 | def initialize(@constraint : String | Regex) 10 | end 11 | 12 | def matches?(request : ::HTTP::Request) 13 | host_parts = request.hostname.to_s.split('.') 14 | last_host_part = host_parts.pop 15 | host_parts.pop unless last_host_part = "localhost" 16 | subdomain = host_parts.join('.') 17 | matches? subdomain, @constraint 18 | end 19 | 20 | private def matches?(subdomain : String, string : String) 21 | subdomain == string 22 | end 23 | 24 | private def matches?(subdomain : String, regex : Regex) 25 | subdomain =~ regex 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/orion/constraints/web_socket_constraint.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | struct Orion::WebSocketConstraint 3 | include Constraint 4 | 5 | def matches?(request : ::HTTP::Request) 6 | return false unless upgrade = request.headers["Upgrade"]? 7 | return false unless upgrade.compare("websocket", case_insensitive: true) == 0 8 | 9 | request.headers.includes_word?("Connection", "Upgrade") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/orion/controller.cr: -------------------------------------------------------------------------------- 1 | require "./cache" 2 | require "./controller/request_helpers" 3 | require "./controller/response_helpers" 4 | require "./controller/rendering" 5 | 6 | # The `Orion::Controller` module can be included in any struct or class to add 7 | # the various helpers methods to make constructing your application easier. 8 | module Orion::Controller 9 | include Rendering 10 | include RequestHelpers 11 | include ResponseHelpers 12 | include CacheHelpers 13 | 14 | # The http context 15 | getter context : Server::Context 16 | 17 | # The websocket, if the controller was initialized from a `ws` route. 18 | getter! websocket : ::HTTP::WebSocket 19 | 20 | # :nodoc: 21 | def initialize(@context, @websocket = nil) 22 | end 23 | end 24 | 25 | require "./controller/base" 26 | -------------------------------------------------------------------------------- /src/orion/controller/base.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | abstract class Orion::Controller::Base 3 | include Orion::Controller 4 | end 5 | -------------------------------------------------------------------------------- /src/orion/controller/cache_helpers.cr: -------------------------------------------------------------------------------- 1 | module Orion::Controller::CacheHelpers 2 | def cache 3 | @context.config.cache 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /src/orion/controller/rendering.cr: -------------------------------------------------------------------------------- 1 | require "../view" 2 | 3 | # This module handles all rendering inside your controllers. 4 | module Orion::Controller::Rendering 5 | include Orion::View 6 | 7 | # Render json 8 | macro render(*, json) 9 | response.content_type = "application/json" 10 | {{ json }}.to_json(response) 11 | end 12 | 13 | # Render plain text 14 | macro render(*, text, content_type = "text/plain") 15 | response.content_type = "text/plain" 16 | response.puts({{ text }}) 17 | end 18 | 19 | # :nodoc: 20 | def __name__ 21 | self.class.name.underscore 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/orion/controller/request_helpers.cr: -------------------------------------------------------------------------------- 1 | module Orion::Controller::RequestHelpers 2 | # The `HTTP::Request` object. 3 | def request 4 | @context.request 5 | end 6 | 7 | # The query params of the `HTTP::Request`. 8 | def query_params 9 | request.query_params 10 | end 11 | 12 | # The path params of the `HTTP::Request` if any routes have named params in the path. 13 | def path_params 14 | request.path_params 15 | end 16 | 17 | # The remote address of the incoming `HTTP::Request`. 18 | def remote_address 19 | request.remote_address 20 | end 21 | 22 | # The resource of the `HTTP::Request`. 23 | def resource 24 | request.resource 25 | end 26 | 27 | # The hostname of the `HTTP::Request`. 28 | def hostname 29 | request.hostname 30 | end 31 | 32 | # The host with port of the `HTTP::Request`. 33 | def host 34 | request.headers["Host"]? 35 | end 36 | 37 | # Format of the `HTTP::Request` 38 | def format 39 | request.format 40 | end 41 | 42 | # Formats of the `HTTP::Request` 43 | def formats 44 | request.formats 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/orion/controller/response_helpers.cr: -------------------------------------------------------------------------------- 1 | module Orion::Controller::ResponseHelpers 2 | # The `HTTP:Response`. 3 | def response 4 | @context.response 5 | end 6 | 7 | # Set the status of the response 8 | def status=(status) 9 | response.status = status 10 | end 11 | 12 | # Set the status of the response 13 | def status_code=(status_code) 14 | response.status_code = status_code 15 | end 16 | 17 | # Set the content type of the response 18 | def content_type=(content_type) 19 | response.content_type = content_type 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/orion/dsl.cr: -------------------------------------------------------------------------------- 1 | require "./dsl/*" 2 | 3 | # The `Orion::DSL` module contains all the macros and methods that are available 4 | # when creating an Orion application. See the various submodules to see each 5 | # macro and/or method you can invoke when building your app. 6 | module Orion::DSL 7 | # Setup the router 8 | private macro included 9 | module RouteHelpers 10 | extend self 11 | end 12 | 13 | # :nodoc: 14 | ROUTER_INITIALIZED = true 15 | # :nodoc: 16 | BASE_PATH = "/" 17 | # :nodoc: 18 | CONSTRAINTS = [] of ::Orion::Constraint 19 | # :nodoc: 20 | CONCERNS = {} of Symbol => String 21 | # :nodoc: 22 | HANDLERS = [] of HTTP::Handler 23 | # :nodoc: 24 | PREFIXES = [] of String 25 | # :nodoc: 26 | TREE = Tree.new 27 | # :nodoc: 28 | CONTROLLER = BaseController 29 | # :nodoc: 30 | ORION_CONFIG = ::Orion::Config.new 31 | 32 | # Add the controller 33 | class BaseController < Orion::Controller::Base 34 | include RouteHelpers 35 | end 36 | 37 | def self.config 38 | ORION_CONFIG 39 | end 40 | 41 | def self.new(*args, **opts) 42 | ::Orion::Router.new(TREE, *args, **opts) 43 | end 44 | 45 | def self.start 46 | start config: config 47 | end 48 | 49 | def self.start(*args, **opts) 50 | ::Orion::Router.start(TREE, *args, **opts) 51 | end 52 | 53 | include ::Orion::DSL::Macros 54 | 55 | use ::Orion::Handlers::Config.new(ORION_CONFIG) 56 | 57 | macro finished 58 | {% if @type.stringify == "" %} 59 | start unless ::Orion::FLAGS["started"]? 60 | {% end %} 61 | end 62 | end 63 | 64 | # :nodoc: 65 | alias Tree = Oak::Tree(Action) 66 | # :nodoc: 67 | alias Context = ::Orion::Server::Context 68 | # :nodoc: 69 | alias Response = ::Orion::Server::Response 70 | # :nodoc: 71 | alias Request = ::Orion::Server::Request 72 | # :nodoc: 73 | alias WebSocket = HTTP::WebSocket 74 | 75 | # :nodoc: 76 | CONTROLLER = nil 77 | 78 | # :nodoc: 79 | def self.normalize_path(*, base_path : String, path : String) 80 | return base_path if path.empty? 81 | parts = [base_path, path].map(&.to_s) 82 | String.build do |str| 83 | parts.each_with_index do |part, index| 84 | part.check_no_null_byte 85 | 86 | str << "/" if index > 0 87 | 88 | byte_start = 0 89 | byte_count = part.bytesize 90 | 91 | if index > 0 && part.starts_with?("/") 92 | byte_start += 1 93 | byte_count -= 1 94 | end 95 | 96 | if index != parts.size - 1 && part.ends_with?("/") 97 | byte_count -= 1 98 | end 99 | 100 | str.write part.unsafe_byte_slice(byte_start, byte_count) 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /src/orion/dsl/concerns.cr: -------------------------------------------------------------------------------- 1 | # Concerns allow you to create a pattern or concern that you wish 2 | # 3 | # to repeat across scopes or resources in your router. 4 | # #### Defining a concern 5 | # 6 | # To define a concern call `concern` with a `Symbol` for the name. 7 | # 8 | # ``` 9 | # concern :authenticated do 10 | # use Authentication.new 11 | # end 12 | # ``` 13 | # #### Using concerns 14 | # 15 | # Once a concern is defined you can call `implements` with a named concern from 16 | # anywhere in your router. 17 | # 18 | # ``` 19 | # concern :authenticated do 20 | # use Authentication.new 21 | # end 22 | 23 | # scope "users" do 24 | # implements :authenticated 25 | # get ":id" 26 | # end 27 | # ``` 28 | module Orion::DSL::Concerns 29 | macro concern(name, &block) 30 | {% CONCERNS[name] = block.body.stringify %} 31 | end 32 | 33 | macro implements(*names) 34 | {% for name in names %} 35 | {{ CONCERNS[name].id }} 36 | {% end %} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /src/orion/dsl/constraints.cr: -------------------------------------------------------------------------------- 1 | # Constraints can be used to further determine if a route is hit beyond just it's path. Routes have some predefined constraints you can specify, but you can also 2 | # pass in a custom constraint. 3 | # 4 | # #### Parameter constraints 5 | # 6 | # When defining a route, you can pass in parameter constraints. The path params will 7 | # be checked against the provided regex before the route is chosen as a valid route. 8 | # 9 | # ``` 10 | # get "users/:id", constraints: {id: /[0-9]{4}/} 11 | # ``` 12 | # 13 | # #### Format constraints 14 | # 15 | # You can constrain the request to a certain format. Such as restricting 16 | # the extension of the URL to '.json'. 17 | # 18 | # ``` 19 | # get "api/users/:id", format: "json" 20 | # ``` 21 | # 22 | # #### Request Mime-Type constraints 23 | # 24 | # You can constrain the request to a certain mime-type by using the `content_type` param 25 | # on the route. This will ensure that if the request has a body, it will provide the proper 26 | # content type. 27 | # 28 | # ``` 29 | # put "api/users/:id", content_type: "application/json" 30 | # ``` 31 | # 32 | # #### Response Mime-Type constraints 33 | # 34 | # You can constrain the response to a certain mime-type by using the `accept` param 35 | # on the route. This is similar to the format constraint but allows clients to 36 | # specify the `Accept` header rather than the extension. 37 | # 38 | # > Orion will automatically add mime-type headers for requests with no Accept header and a specified extension. 39 | # 40 | # ``` 41 | # get "api/users/:id", accept: "application/json" 42 | # ``` 43 | # 44 | # #### Combined Mime-Type constraints 45 | # 46 | # You can constrain the request and response to a certain mime-type by using the `type` param 47 | # on the route. This will ensure that if the request has a body, it will provide the proper 48 | # content type. In addition, it will also validate that the client provides a proper 49 | # accept header for the response. 50 | # 51 | # Orion will automatically add mime-type headers for requests with no Accept header and 52 | # a specified extension. 53 | # 54 | # ``` 55 | # put "api/users/:id", type: "application/json" 56 | # ``` 57 | # 58 | # #### Host constraints 59 | # 60 | # You can constrain the request to a specific host by wrapping routes 61 | # in a `host` block. In this method, any routes within the block will be 62 | # matched at that constraint. 63 | # 64 | # You may also choose to limit the request to a certain format. Such as restricting 65 | # the extension of the URL to '.json'. 66 | # 67 | # ``` 68 | # host "example.com" do 69 | # get "users/:id", format: "json" 70 | # end 71 | # ``` 72 | # 73 | # #### Subdomain constraints 74 | # 75 | # You can constrain the request to a specific subdomain by wrapping routes 76 | # in a `subdomain` block. In this method, any routes within the block will be 77 | # matched at that constraint. 78 | # 79 | # You may also choose to limit the request to a certain format. Such as restricting 80 | # the extension of the URL to '.json'. 81 | # 82 | # ``` 83 | # subdomain "api" do 84 | # get "users/:id", format: "json" 85 | # end 86 | # ``` 87 | # 88 | # #### Custom Constraints 89 | # 90 | # You can also pass in your own constraints by just passing a class/struct that 91 | # implements the `Orion::Constraint` module. 92 | # 93 | # ``` 94 | # struct MyConstraint 95 | # def matches?(req : HTTP::Request) 96 | # true 97 | # end 98 | # end 99 | # 100 | # constraint MyConstraint.new do 101 | # get "users/:id", format: "json" 102 | # end 103 | # ``` 104 | module Orion::DSL::Constraints 105 | # :nodoc: 106 | CONSTRAINTS = [] of Constraint 107 | 108 | # Constrain routes by an `Orion::Constraint` 109 | macro constraint(constraint) 110 | constraints({{ constraint }}) do 111 | {{ yield }} 112 | end 113 | end 114 | 115 | # Constrain routes by one or more `Orion::Constraint`s 116 | macro constraints(*constraints) 117 | scope do 118 | {% for constraint, i in constraints %} # Add the array of provided constraints 119 | CONSTRAINTS << {{ constraint }} 120 | {% end %} 121 | {{ yield }} 122 | end 123 | end 124 | 125 | # Constrain routes by a given domain 126 | macro host(host) 127 | constraint(::Orion::HostConstraint.new({{ host }})) do 128 | {{ yield }} 129 | end 130 | end 131 | 132 | # Constrain routes by a given subdomain 133 | macro subdomain(subdomain) 134 | constraint(::Orion::SubdomainConstraint.new({{ subdomain }})) do 135 | {{ yield }} 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /src/orion/dsl/handlers.cr: -------------------------------------------------------------------------------- 1 | # Handlers allow you to maniplate the request stack by passing instances of classes 2 | # implementing the 3 | # [`HTTP::Handler`](https://crystal-lang.org/api/HTTP/Handler.html) _(a.k.a. middleware)_ 4 | # module. 5 | # 6 | # > Handlers will only apply to the routes specified below them, so be sure to place your handlers near the top of your route. 7 | # 8 | # ``` 9 | # use HTTP::ErrorHandler 10 | # use HTTP::LogHandler.new(File.open("tmp/application.log")) 11 | # ``` 12 | # 13 | # ### Nested Routes using `scope` 14 | # 15 | # Scopes are a method in which you can nest routes under a common path. This prevents 16 | # the need for duplicating paths and allows a developer to easily change the parent 17 | # of a set of child paths. 18 | # 19 | # ``` 20 | # scope "users" do 21 | # root to: "Users#index" 22 | # get ":id", to: "Users#show" 23 | # delete ":id", to: "Users#destroy" 24 | # end 25 | # ``` 26 | # 27 | # #### Handlers within nested routes 28 | # 29 | # Instances of link:https://crystal-lang.org/api/HTTP/Handler.html[`HTTP::Handler`] can be 30 | # used within a `scope` block and will only apply to the subsequent routes within that scope. 31 | # It is important to note that the parent context's handlers will also be used. 32 | # 33 | # > Handlers will only apply to the routes specified below them, so be sure to place your handlers near the top of your scope. 34 | # 35 | # ``` 36 | # scope "users" do 37 | # use AuthorizationHandler.new 38 | # root to: "Users#index" 39 | # get ":id", to: "Users#show" 40 | # delete ":id", to: "Users#destroy" 41 | # end 42 | # ``` 43 | module Orion::DSL::Handlers 44 | # Insert a new handler. This is the same as `handlers.push(HTTP::LogHandler.new)` 45 | macro use(handler) 46 | HANDLERS << case (handler = {{handler}}) 47 | when HTTP::Handler 48 | handler 49 | else 50 | handler.new 51 | end 52 | end 53 | 54 | # Direct access the handlers array, giving you access to methods like `unshift`, 55 | # `push` and `clear`. 56 | macro handlers 57 | HANDLERS 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /src/orion/dsl/helpers.cr: -------------------------------------------------------------------------------- 1 | # Route helpers provide type-safe methods to generate paths and URLs to defined routes 2 | # in your application. By including the `Helpers` module on the router (i.e. `MyApplicationRouter::Helpers`) 3 | # you can access any helper defined in the router by `{{name}}_path` to get its corresponding 4 | # route. In addition, when you have a `@context : HTTP::Server::Context` instance var, 5 | # you will also be able to access a `{{name}}_url` to get the full URL. 6 | # 7 | # ``` 8 | # scope "users", helper_prefix: "user" do 9 | # get "/new", to: "users#new", helper: "new" 10 | # end 11 | # 12 | # class UsersController < BaseController 13 | # def new 14 | # File.open("new.html") { |f| IO.copy(f, response) } 15 | # end 16 | # 17 | # def show 18 | # user = User.find(request.path_params["id"]) 19 | # response.headers["Location"] = new_user_path 20 | # response.status_code = 301 21 | # response.close 22 | # end 23 | # end 24 | # ``` 25 | # 26 | # #### Making route helpers from your routes 27 | # 28 | # In order to make a helper from your route, you can use the `helper` named argument in your route. 29 | # 30 | # ``` 31 | # scope "users" do 32 | # get "/new", to: "Users#new", helper: "new" 33 | # end 34 | # ``` 35 | # 36 | # #### Using route helpers in your code 37 | # 38 | # As you add helpers they are added to the nested `Helpers` module of your router. 39 | # you may include this module anywhere in your code to get access to the methods, 40 | # or call them on the module directly. 41 | # 42 | # _If `@context : HTTP::Server::Context` is present in the class, you will also be 43 | # able to use the `{helper}_url` versions of the helpers._ 44 | # 45 | # ``` 46 | # resources :users 47 | # 48 | # class User 49 | # include RouteHelpers 50 | # 51 | # def route 52 | # user_path user_id: self.id 53 | # end 54 | # end 55 | # 56 | # puts RouteHelpers.users_path 57 | # ``` 58 | module Orion::DSL::Helpers 59 | private macro define_helper(*, base_path, path, spec) 60 | {% name_parts = PREFIXES + [] of StringLiteral %} 61 | 62 | {% if spec.is_a? BoolLiteral %} 63 | {% raise "Cannot use a boolean helper outside of a scope." if PREFIXES.size == 0 %} 64 | {% elsif spec.is_a?(NamedTupleLiteral) || spec.is_a?(HashLiteral) %} 65 | {% name_parts.unshift(spec[:prefix]) if spec[:prefix] %} 66 | {% name_parts.push(spec[:name]) if spec[:name] %} 67 | {% name_parts.push(spec[:suffix]) if spec[:suffix] %} 68 | {% elsif spec.is_a? StringLiteral %} 69 | {% name_parts.push(spec) if spec %} 70 | {% else %} 71 | {% raise "Unsupported spec type: #{spec.class_name}" %} 72 | {% end %} 73 | 74 | {% method_name = name_parts.map(&.id).join("_").id %} 75 | 76 | module ::{{ RouteHelpers }} 77 | # Returns the full path for `{{ method_name.id }}` 78 | def self.{{ method_name.id }}_path(**params) 79 | path = ::Orion::DSL.normalize_path(base_path: {{ base_path }}, path: {{ path }}) 80 | result = TREE.find(path).not_nil! 81 | path_param_names = result.params.keys 82 | 83 | {% "Convert all the params to a string" %} 84 | params_hash = ({} of String => String).tap do |memo| 85 | params.each do |key, value| 86 | memo[key.to_s] = value.to_s 87 | result.params.delete key.to_s 88 | end 89 | end 90 | 91 | raise Orion::ParametersMissing.new(result.params.keys) unless result.params.keys.empty? 92 | 93 | # Assign the path params 94 | path_param_names.each do |name| 95 | path = path.gsub /(:|\*)#{name}/, params_hash[name] 96 | params_hash.delete name 97 | end 98 | 99 | query = HTTP::Params.encode(params_hash) unless params_hash.empty? 100 | 101 | URI.new(path: path, query: query).to_s 102 | end 103 | 104 | def {{ method_name.id }}_path(**params) 105 | ::{{ RouteHelpers }}.{{ method_name.id }}_path(**params) 106 | end 107 | 108 | # Returns the full url for `{{ method_name.id }}` 109 | def {{ method_name.id }}_url(**params) 110 | uri = URI.parse {{ method_name.id }}_path(**params, host: request.headers["Host"]?) 111 | uri.host = @context.request.headers["Host"]? 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /src/orion/dsl/macros.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Orion::DSL::Macros 3 | private macro included 4 | include ::Orion::DSL::Concerns 5 | include ::Orion::DSL::Constraints 6 | include ::Orion::DSL::Handlers 7 | include ::Orion::DSL::Helpers 8 | include ::Orion::DSL::Match 9 | include ::Orion::DSL::Mount 10 | include ::Orion::DSL::RequestMethods 11 | include ::Orion::DSL::WebSockets 12 | include ::Orion::DSL::Root 13 | include ::Orion::DSL::Scope 14 | include ::Orion::DSL::Resources 15 | include ::Orion::DSL::Static 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/orion/dsl/match.cr: -------------------------------------------------------------------------------- 1 | # Catch-all routes using `match` 2 | # 3 | # In some instances, you may just want to redirect all verbs to a particular 4 | # controller and action. 5 | # 6 | # You can use the `match` method and pass it's route and 7 | # any variation of the [Generic Route Arguments](#generic-route-arguments). 8 | # 9 | # ``` 10 | # match "404", controller: ErrorsController, action: error_404 11 | # ``` 12 | # 13 | # ### Generic route arguments 14 | # There are a variety of ways that you can interact with basic routes. Below are 15 | # some examples and guidelines on the different ways you can interact with the router. 16 | # #### Using `to: String` to target a controller and action 17 | # One of the most common ways we will be creating routes in this guide is to use 18 | # the `to` argument supplied with a controller and action in the form of a string. 19 | # In the example below `users#create` will map to `UsersController.new(cxt : Orion::Server::Context).create`. 20 | # You can also pass an exact constant name. 21 | # 22 | # ``` 23 | # post "users", to: "users#create" 24 | # ``` 25 | # 26 | # #### Using `controller: Type` and `action: Method` 27 | # A longer form of the `to` argument strategy above allows us to pass the controller and action 28 | # independently. 29 | # 30 | # ``` 31 | # post "users", controller: UsersController, action: create 32 | # ``` 33 | # 34 | # #### Using block syntax 35 | # Sometimes, we may want a more link:https://github.com/kemalcr/kemal[kemal] or 36 | # link:http://sinatrarb.com/[sinatra] like approach. To accomplish this, we can 37 | # simply pass a block that will be evaluated as a controller. 38 | # 39 | # ``` 40 | # post "users" do 41 | # "Foo" 42 | # end 43 | # ``` 44 | # 45 | # #### Using a `call` able object 46 | # Lastly a second argument can be any 47 | # object that responds to `#call(cxt : Orion::Server::Context)`. 48 | # 49 | # ``` 50 | # post "users", ->(context : Orion::Server::Context) { 51 | # context.response.puts "foo" 52 | # } 53 | # ``` 54 | module Orion::DSL::Match 55 | # Defines a match route to a callable object. 56 | # 57 | # You can route to any object that responds to `call` with an `Orion::Server::Context`. 58 | # 59 | # ``` 60 | # module Callable 61 | # def call(cxt : Orion::Server::Context) 62 | # # ... do something 63 | # end 64 | # end 65 | # 66 | # match "/path", Callable 67 | # ``` 68 | macro match(path, callable, *, via = :all, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 69 | {% if !format && path.split(".").size > 1 %} 70 | {% format = path.split(".")[-1] %} 71 | {% path = path.split(".")[0..-2].join(".") %} 72 | {% end %} 73 | 74 | # create the action 75 | %action = ::Orion::Action.new( 76 | -> (context : ::Orion::Server::Context) { 77 | write_tracker = ::Orion::WriteTracker.new 78 | output = context.response.output 79 | context.response.output = ::IO::MultiWriter.new(output, write_tracker, sync_close: true) 80 | return_value = {{ callable }}.call(context) 81 | # If no response has been written handle the return value as a response 82 | if !write_tracker.written 83 | case return_value 84 | when String 85 | context.response.puts return_value 86 | when IO 87 | is_invalid = return_value.closed? || return_value == context.response || context.response.output || context.response.@original_output 88 | IO.copy(return_value, context.response) unless is_invalid 89 | else 90 | end 91 | end 92 | nil 93 | }, 94 | handlers: HANDLERS, 95 | constraints: CONSTRAINTS 96 | ) 97 | 98 | # Add the route to the tree 99 | TREE.add( 100 | ::Orion::DSL.normalize_path(base_path: {{ BASE_PATH }}, path: {{ path }}), 101 | %action 102 | ) 103 | 104 | {% if helper %} # Define the helper 105 | define_helper(base_path: {{ BASE_PATH }}, path: {{ path }}, spec: {{ helper }}) 106 | {% end %} 107 | 108 | {% if constraints %} # Define the param constraints 109 | %action.constraints.unshift ::Orion::ParamsConstraint.new({{ constraints }}.to_h) 110 | {% end %} 111 | 112 | {% if content_type %} # Define the content type constraint 113 | %action.constraints.unshift ::Orion::ContentTypeConstraint.new({{ content_type }}) 114 | {% end %} 115 | 116 | {% if type %} # Define the content type and accept constraint 117 | %action.constraints.unshift ::Orion::ContentTypeConstraint.new({{ type }}) 118 | %action.constraints.unshift ::Orion::AcceptConstraint.new({{ type }}) 119 | {% end %} 120 | 121 | {% if format %} # Define the format constraint 122 | %action.constraints.unshift ::Orion::FormatConstraint.new({{ format }}) 123 | {% end %} 124 | 125 | {% if accept %} # Define the content type constraint 126 | %action.constraints.unshift ::Orion::AcceptConstraint.new({{ accept }}) 127 | {% end %} 128 | 129 | {% if via != :all && !via.nil? %} # Define the method constraint 130 | %action.constraints.unshift ::Orion::RequestMethodsConstraint.new({{ via }}) 131 | {% end %} 132 | end 133 | 134 | # Defines a match route to a controller and action (short form). 135 | # You can route to a controller and action by passing the `to` argument in 136 | # the form of `"#action"`. 137 | # 138 | # ``` 139 | # class MyController 140 | # def new(@context : Orion::Server::Context) 141 | # end 142 | # 143 | # def match 144 | # # ... do something 145 | # end 146 | # end 147 | # 148 | # match "/path", to: "My#match" 149 | # ``` 150 | macro match(path, *, to, via = :all, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 151 | {% parts = to.split("#") %} 152 | {% controller = run("../inflector/controllerize.cr", parts[0].id) %} 153 | {% action = parts[1] %} 154 | {% raise("`to` must be in the form `controller#action`") unless controller && action && parts.size == 2 %} 155 | match({{ path }}, controller: {{ controller.id }}, action: {{ action.id }}, via: {{ via }}, helper: {{ helper }}, constraints: {{ constraints }}, format: {{ format }}, accept: {{ accept }}, content_type: {{ content_type }}, type: {{ type }}) 156 | end 157 | 158 | # Defines a match route to a controller and action (long form). 159 | # You can route to a controller and action by passing the `controller` and 160 | # `action` arguments, if action is omitted it will default to `match`. 161 | # 162 | # ``` 163 | # class MyController 164 | # def new(@context : Orion::Server::Context) 165 | # end 166 | # 167 | # def match 168 | # # ... do something 169 | # end 170 | # end 171 | # 172 | # match "/path", controller: MyController, action: match 173 | # ``` 174 | macro match(path, *, action, controller = CONTROLLER, via = :all, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 175 | match({{ path }}, ->(context : Orion::Server::Context) { {{ controller }}.new(context).{{ action }} }, via: {{ via }}, helper: {{ helper }}, constraints: {{ constraints }}, format: {{ format }}, accept: {{ accept }}, content_type: {{ content_type }}, type: {{ type }}) 176 | end 177 | 178 | # Defines a match route with a block. 179 | # 180 | # When given with 0 argument it will yield the block and have access to any method within the BaseController of the application. 181 | # When given with 1 argument it will yield the block with `Orion::Server::Context` and have access to any method within the BaseController of the application. 182 | # When given with 2 arguments it will yield the block with `HTTP::Request` and `HTTP::Server::Response` and have access to any method within the BaseController of the application. 183 | # 184 | # ``` 185 | # match "/path" do |context| 186 | # # ... do something 187 | # end 188 | # ``` 189 | macro match(path, *, via = :all, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil, &block) 190 | {% controller_const = run "../inflector/random_const.cr", "Controller" %} 191 | struct {{ controller_const }} 192 | include ::Orion::Controller 193 | include RouteHelpers 194 | 195 | def handle 196 | {% if block.args.size == 0 %} 197 | {{ block.body }} 198 | {% elsif block.args.size == 1 %} 199 | action_block = ->({{ "#{block.args[0]} : Orion::Server::Context".id }}){ 200 | {{ block.body }} 201 | } 202 | action_block.call(context) 203 | {% elsif block.args.size == 2 %} 204 | action_block = ->({{ "#{block.args[0]} : HTTP::Request, #{block.args[1]} : HTTP::Server::Response".id }}){ 205 | {{ block.body }} 206 | } 207 | action_block.call(request, response) 208 | {% else %} 209 | {% raise "block must have 0..2 arguments" %} 210 | {% end %} 211 | end 212 | end 213 | match({{ path }}, controller: {{ controller_const }}, action: handle, via: {{ via }}, helper: {{ helper }}, constraints: {{ constraints }}, format: {{ format }}, accept: {{ accept }}, content_type: {{ content_type }}, type: {{ type }}) 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /src/orion/dsl/mount.cr: -------------------------------------------------------------------------------- 1 | # You can mount other orion and crystal applications directly within your app. 2 | # This can be useful when separating out concerns between different functional 3 | # areas of your application. 4 | module Orion::DSL::Mount 5 | # Mount an application at the specified path. 6 | macro mount(app, *, at = "/") 7 | scope {{ at }} do 8 | use ::Orion::Handlers::ResetPath.new 9 | match("/*", {{ app }}) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/orion/dsl/request_methods.cr: -------------------------------------------------------------------------------- 1 | # Request method macros are shorthard ways of constraining a request to a single 2 | # request method. You can read more about the options available to each of these 3 | # macros in the `Orion::DSL::Match`. 4 | module Orion::DSL::RequestMethods 5 | METHODS = %w{GET HEAD POST PUT DELETE CONNECT OPTIONS TRACE PATCH} 6 | 7 | {% for method in METHODS %} 8 | # Defines a {{ method.id }} route to a callable object. 9 | # 10 | # You can route to any object that responds to `call` with an `HTTP::Server::Context`, 11 | # this also works for any `Proc(HTTP::Server::Context, _)`. 12 | # 13 | # ``` 14 | # module Callable 15 | # def call(cxt : HTTP::Server::Context) 16 | # # ... do something 17 | # end 18 | # end 19 | # 20 | # match "/path", Callable 21 | # ``` 22 | macro {{ method.downcase.id }}(path, callable, *, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 23 | match(\{{ path }}, \{{ callable }}, via: {{ method.downcase }}, helper: \{{ helper }}, constraints: \{{ constraints }}, format: \{{ format }}, accept: \{{ accept }}, content_type: \{{ content_type }}, type: \{{ type }}) 24 | end 25 | 26 | # Defines a {{ method.id }} route to a controller and action (short form). 27 | # You can route to a controller and action by passing the `to` argument in 28 | # the form of `"#action"`. 29 | # 30 | # ``` 31 | # class MyController < BaseController 32 | # def match 33 | # # ... do something 34 | # end 35 | # end 36 | # 37 | # match "/path", to: "My#{{ method.downcase.id }}" 38 | # ``` 39 | macro {{ method.downcase.id }}(path, *, to, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 40 | match(\{{ path }}, to: \{{ to }}, via: {{ method.downcase }}, helper: \{{ helper }}, constraints: \{{ constraints }}, format: \{{ format }}, accept: \{{ accept }}, content_type: \{{ content_type }}, type: \{{ type }}) 41 | end 42 | 43 | # Defines a {{ method.id }} route to a controller and action (long form). 44 | # You can route to a controller and action by passing the `controller` and 45 | # `action` arguments, if action is omitted it will default to `match`. 46 | # 47 | # ``` 48 | # class MyController 49 | # def new(@context : HTTP::Server::Context) 50 | # end 51 | # 52 | # def match 53 | # # ... do something 54 | # end 55 | # end 56 | # 57 | # match "/path", controller: MyController, action: {{ method.downcase.id }} 58 | # ``` 59 | macro {{ method.downcase.id }}(path, *, action, controller = CONTROLLER, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 60 | match(\{{ path }}, controller: \{{ controller }}, action: \{{ action }}, via: {{ method.downcase }}, helper: \{{ helper }}, constraints: \{{ constraints }}, format: \{{ format }}, accept: \{{ accept }}, content_type: \{{ content_type }}, type: \{{ type }}) 61 | end 62 | 63 | # Defines a {{ method.id }} route with a block. 64 | # 65 | # You can pass a block. Each block will be evaluated as a controller method 66 | # and have access to all controller helper methods. 67 | # 68 | # ``` 69 | # match "/path" do 70 | # # ... do something 71 | # end 72 | # ``` 73 | macro {{ method.downcase.id }}(path, *, helper = nil, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil, &block) 74 | match(\{{ path }}, via: {{ method.downcase }}, helper: \{{ helper }}, constraints: \{{ constraints }}, format: \{{ format }}, accept: \{{ accept }}, content_type: \{{ content_type }}, type: \{{ type }}) \{{block}} 75 | end 76 | {% end %} 77 | end 78 | -------------------------------------------------------------------------------- /src/orion/dsl/resources.cr: -------------------------------------------------------------------------------- 1 | # A common way in Orion to route is to do so against a known resource. This method 2 | # will create a series of routes targeted at a specific controller. 3 | # 4 | # _The following is an example controller definition and the matching 5 | # resources definition._ 6 | # 7 | # ``` 8 | # class PostsController 9 | # include Orion::ControllerHelper 10 | # include ResponseHelpers 11 | # 12 | # def index 13 | # @posts = Post.all 14 | # render :index 15 | # end 16 | # 17 | # def new 18 | # @post = Post.new 19 | # render :new 20 | # end 21 | # 22 | # def create 23 | # post = Post.create(request) 24 | # redirect to: post_path post_id: post.id 25 | # end 26 | # 27 | # def show 28 | # @post = Post.find(request.path_params["post_id"]) 29 | # end 30 | # 31 | # def edit 32 | # @post = Post.find(request.path_params["post_id"]) 33 | # render :edit 34 | # end 35 | # 36 | # def update 37 | # post = Post.find(request.path_params["post_id"]) 38 | # HTTP::FormData.parse(request) do |part| 39 | # post.attributes[part.name] = part.body.gets_to_end 40 | # end 41 | # redirect to: post_path post_id: post.id 42 | # end 43 | # 44 | # def delete 45 | # post = Post.find(request.path_params["post_id"]) 46 | # post.delete 47 | # redirect to: posts_path 48 | # end 49 | # end 50 | # 51 | # resources :posts 52 | # ``` 53 | # 54 | # #### Including/Excluding Actions 55 | # 56 | # By default, the actions `index`, `new`, `create`, `show`, `edit`, `update`, `delete` 57 | # are included. You may include or exclude explicitly by using the `only` and `except` params. 58 | # 59 | # > NOTE: The index action is not added for [singular resources](#singular-resources). 60 | # 61 | # ``` 62 | # resources :posts, except: [:edit, :update] 63 | # resources :users, only: [:new, :create, :show] 64 | # ``` 65 | # 66 | # #### Nested Resources and Routes 67 | # 68 | # You can add nested resources and member routes by providing a block to the 69 | # `resources` definition. 70 | # 71 | # ``` 72 | # resources :posts do 73 | # post "feature", action: feature 74 | # resources :likes 75 | # resources :comments 76 | # end 77 | # ``` 78 | # 79 | # #### Singular Resources 80 | # 81 | # In addition to using the collection of `resources` method, You can also add 82 | # singular resources which do not provide a `id_param` or `index` action. 83 | # 84 | # ``` 85 | # resource :profile 86 | # ``` 87 | # 88 | # #### Customizing ID 89 | # 90 | # You can customize the ID path parameter by passing the `id_param` parameter. 91 | # 92 | # ``` 93 | # resources :posts, id_param: :article_id 94 | # ``` 95 | # 96 | # #### Constraining the ID 97 | # 98 | # You can set constraints on the ID parameter by passing the `id_constraint` parameter. 99 | # 100 | # See `Orion::DSL::Constraints` for more details on constraints. 101 | # 102 | # ``` 103 | # resources :posts, id_constraint: /^\d{4}$/ 104 | # ``` 105 | module Orion::DSL::Resources 106 | macro resources(name, *, controller = nil, only = nil, except = nil, id_constraint = nil, format = nil, accept = nil, id_param = nil, content_type = nil, type = nil) 107 | {% raise "resource name must be a symbol" unless name.is_a? SymbolLiteral %} 108 | {% name = name.id %} 109 | {% singular_name = run "../inflector/singularize.cr", name %} 110 | {% id_param = (id_param || "#{singular_name.id}_id").id.stringify %} 111 | {% singular_underscore_name = run("../inflector/underscore.cr", singular_name) %} 112 | {% controller = controller || run("../inflector/controllerize.cr", name.downcase) %} 113 | {% underscore_name = run("../inflector/underscore.cr", name) %} 114 | 115 | scope {{ "/#{name}" }}, controller: {{ controller }} do 116 | {% if content_type %} # Define the content type constraint 117 | CONSTRAINTS << ::Orion::ContentTypeConstraint.new({{ content_type }}) 118 | {% end %} 119 | 120 | {% if type %} # Define the content type and accept constraint 121 | CONSTRAINTS << ::Orion::ContentTypeConstraint.new({{ type }}) 122 | CONSTRAINTS << ::Orion::AcceptConstraint.new({{ type }}) 123 | {% end %} 124 | 125 | {% if format %} # Define the format constraint 126 | CONSTRAINTS << ::Orion::FormatConstraint.new({{ format }}) 127 | {% end %} 128 | 129 | {% if accept %} # Define the accept constraint 130 | CONSTRAINTS << ::Orion::AcceptConstraint.new({{ accept }}) 131 | {% end %} 132 | 133 | scope helper_prefix: {{ underscore_name }} do 134 | resource_action(:get, "/", :index, only: {{ only }}, except: {{ except }}, helper: true) 135 | resource_action(:post, "/", :create, only: {{ only }}, except: {{ except }}) 136 | end 137 | 138 | scope helper_prefix: {{ singular_underscore_name }} do 139 | resource_action(:get, "/new", :new, only: {{ only }}, except: {{ except }}, helper: { prefix: "new" }) 140 | end 141 | 142 | scope {{ "/:#{id_param.id}" }}, helper_prefix: {{ singular_underscore_name.stringify }} do 143 | {% if id_constraint %} 144 | CONSTRAINTS << ::Orion::ParamsConstraint.new({ {{ id_param }} => {{ id_constraint }} }) 145 | {% end %} 146 | 147 | resource_action(:get, "", :show, only: {{ only }}, except: {{ except }}, helper: true) 148 | resource_action(:get, "/edit", :edit, only: {{ only }}, except: {{ except }}, helper: { prefix: "edit" }) 149 | resource_action(:put, "", :update, only: {{ only }}, except: {{ except }}) 150 | resource_action(:patch, "", :update, only: {{ only }}, except: {{ except }}) 151 | resource_action(:delete, "", :delete, only: {{ only }}, except: {{ except }}) 152 | 153 | {{ yield }} 154 | end 155 | end 156 | end 157 | 158 | macro resource(name, *, controller = nil, only = nil, except = nil, format = nil, accept = nil, content_type = nil, type = nil) 159 | {% raise "resource name must be a symbol" unless name.is_a? SymbolLiteral %} 160 | {% name = name.id %} 161 | {% controller = controller || run("../inflector/controllerize.cr", name.downcase) %} 162 | {% underscore_name = run("../inflector/underscore.cr", name) %} 163 | 164 | scope {{ "/#{name}" }}, helper_prefix: {{ underscore_name }}, controller: {{ controller }} do 165 | {% if content_type %} # Define the content type constraint 166 | CONSTRAINTS << ::Orion::ContentTypeConstraint.new({{ content_type }}) 167 | {% end %} 168 | 169 | {% if type %} # Define the content type and accept constraint 170 | CONSTRAINTS << ::Orion::ContentTypeConstraint.new({{ type }}) 171 | CONSTRAINTS << ::Orion::AcceptConstraint.new({{ type }}) 172 | {% end %} 173 | 174 | {% if format %} # Define the format constraint 175 | CONSTRAINTS << ::Orion::FormatConstraint.new({{ format }}) 176 | {% end %} 177 | 178 | {% if accept %} # Define the accept constraint 179 | CONSTRAINTS << ::Orion::AcceptConstraint.new({{ accept }}) 180 | {% end %} 181 | 182 | resource_action(:get, "/new", :new, only: {{ only }}, except: {{ except }}, helper: { prefix: "new" }) 183 | resource_action(:post, "", :create, only: {{ only }}, except: {{ except }}) 184 | resource_action(:get, "", :show, only: {{ only }}, except: {{ except }}, helper: true) 185 | resource_action(:get, "/edit", :edit, only: {{ only }}, except: {{ except }}, helper: { prefix: "edit" }) 186 | resource_action(:put, "", :update, only: {{ only }}, except: {{ except }}) 187 | resource_action(:patch, "", :update, only: {{ only }}, except: {{ except }}) 188 | resource_action(:delete, "", :delete, only: {{ only }}, except: {{ except }}) 189 | 190 | {{ yield }} 191 | end 192 | end 193 | 194 | private macro resource_action(method, path, action, *, only = nil, except = nil, helper = false) 195 | {% except = !except || except.is_a?(ArrayLiteral) ? except : [except] %} 196 | {% only = !only || only.is_a?(ArrayLiteral) ? only : [only] %} 197 | {% if (!only || only.includes?(action)) && (!except || !except.includes?(action)) %} 198 | {{ method.id }}({{ path }}, action: {{ action.id }}, helper: {{ helper }}) 199 | {% end %} 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /src/orion/dsl/root.cr: -------------------------------------------------------------------------------- 1 | # The root macro is a shortcut to making a `get "/"` at the root of you application 2 | # or at the root of a scope. 3 | module Orion::DSL::Root 4 | # Define a `GET /` route at the current path with a callable object. 5 | # 6 | # ``` 7 | # module Callable 8 | # def call(cxt : HTTP::Server::Context) 9 | # # ... do something 10 | # end 11 | # end 12 | # 13 | # router MyRouter do 14 | # root Callable 15 | # end 16 | # ``` 17 | macro root(callable, *, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 18 | get("/", {{ callable }}, constraints: {{ constraints }}, format: {{ format }}, accept: {{ accept }}, content_type: {{ content_type }}, type: {{ type }}, helper: "root") 19 | end 20 | 21 | # Define a `GET /` route at the current path with a controller and action (short form). 22 | # 23 | # ``` 24 | # router MyRouter do 25 | # root to: "#action" 26 | # end 27 | # ``` 28 | macro root(*, to, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil) 29 | get("/", to: {{ to }}, constraints: {{ constraints }}, format: {{ format }}, accept: {{ accept }}, content_type: {{ content_type }}, type: {{ type }}, helper: "root") 30 | end 31 | 32 | # Define a `GET /` route at the current path with a controller and action (long form). 33 | # 34 | # ``` 35 | # router MyRouter do 36 | # root controller: Controller action: action 37 | # end 38 | # ``` 39 | macro root(*, action, controller = CONTROLLER, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil, &block) 40 | get("/", action: {{ action }}, controller: {{ controller }}, constraints: {{ constraints }}, format: {{ format }}, accept: {{ accept }}, content_type: {{ content_type }}, type: {{ type }}, helper: "root") 41 | end 42 | 43 | # Define a `GET /` route at the current path with a callable object. 44 | # 45 | # ``` 46 | # router MyRouter do 47 | # root do |context| 48 | # # ... 49 | # end 50 | # end 51 | # ``` 52 | macro root(*, constraints = nil, format = nil, accept = nil, content_type = nil, type = nil, &block) 53 | {% args = block.args.map { |n| "#{n} : HTTP::Server::Context".id }.join(", ").id %} 54 | get("/", constraints: {{ constraints }}, format: {{ format }}, accept: {{ accept }}, content_type: {{ content_type }}, type: {{ type }}, helper: "root") {{block}} 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /src/orion/dsl/scope.cr: -------------------------------------------------------------------------------- 1 | # Scopes are a method in which you can nest routes under a common path. This prevents 2 | # the need for duplicating paths and allows a developer to easily change the parent 3 | # of a set of child paths. 4 | # 5 | # ``` 6 | # router MyApplicationRouter do 7 | # scope "users" do 8 | # root to: "Users#index" 9 | # get ":id", to: "Users#show" 10 | # delete ":id", to: "Users#destroy" 11 | # end 12 | # end 13 | # ``` 14 | # 15 | # #### Handlers within nested routes 16 | # 17 | # Instances of link:https://crystal-lang.org/api/HTTP/Handler.html[`HTTP::Handler`] can be 18 | # used within a `scope` block and will only apply to the subsequent routes within that scope. 19 | # It is important to note that the parent context's handlers will also be used. 20 | # 21 | # > Handlers will only apply to the routes specified below them, so be sure to place your handlers near the top of your scope. 22 | # 23 | # ``` 24 | # router MyApplicationRouter do 25 | # scope "users" do 26 | # use AuthorizationHandler.new 27 | # root to: "Users#index" 28 | # get ":id", to: "Users#show" 29 | # delete ":id", to: "Users#destroy" 30 | # end 31 | # end 32 | # ``` 33 | module Orion::DSL::Scope 34 | # Create a scope, optionall nested under a path. 35 | macro scope(path = nil, helper_prefix = nil, controller = nil) 36 | {% prefixes = PREFIXES + [helper_prefix] if helper_prefix %} 37 | {% scope_const = run "../inflector/random_const.cr", "Scope" %} 38 | 39 | # :nodoc: 40 | module {{ scope_const }} 41 | include ::Orion::DSL::Macros 42 | 43 | CONSTRAINTS = {% if @type.stringify != "" %}::{{ @type }}{% end %}::CONSTRAINTS.dup 44 | HANDLERS = {% if @type.stringify != "" %}::{{ @type }}{% end %}::HANDLERS.dup 45 | 46 | # Set the controller 47 | {% if controller %} 48 | CONTROLLER = {{ controller }} 49 | {% end %} 50 | 51 | # Set the base path 52 | {% if path %} 53 | BASE_PATH = [{% if @type.stringify != "" %}::{{ @type }}{% end %}::BASE_PATH.rchop('/'), {{ path }}.lchop('/')].join('/') 54 | use Orion::Handlers::ScopeBasePath.new(BASE_PATH) 55 | {% else %} 56 | BASE_PATH = {% if @type.stringify != "" %}::{{ @type }}{% end %}::BASE_PATH 57 | {% end %} 58 | 59 | # Setup the helper prefixes 60 | {% if helper_prefix %} 61 | PREFIXES = {{ prefixes }} 62 | {% end %} 63 | 64 | # Yield the block 65 | {{ yield }} 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /src/orion/dsl/static.cr: -------------------------------------------------------------------------------- 1 | require "uuid" 2 | require "crystar" 3 | 4 | # The static macros allows you to bind static content to given path. 5 | # You can use this for strings and/or files that will never change in a given 6 | # release 7 | module Orion::DSL::Static 8 | # Mount a directory of static files. 9 | # 10 | # router MyRouter do 11 | # static dir: "./public", path: "/" 12 | # end 13 | # ``` 14 | macro static(path = "/", *, dir) 15 | {% if flag?(:packagestatics) || (flag?(:release) && !flag?(:dontpackagestatics)) %} 16 | {% dir = dir.gsub(/^\.\//, "") %} 17 | %dir = File.join(Assets.unpack({{ run "../assets/pack.cr", dir, `date +%s%N` }}), {{ dir }}) 18 | {% else %} 19 | %dir = {{ dir }} 20 | {% end %} 21 | mount ::HTTP::StaticFileHandler.new(%dir, false, false), at: {{ path }} 22 | end 23 | 24 | macro static(*, path, string) 25 | %str : String = {{string}} 26 | get {{ path }}, ->(c : ::Orion::Server::Context){ c.response.puts %str } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/orion/dsl/websockets.cr: -------------------------------------------------------------------------------- 1 | # The websocket handlers allow you to easily add websocket support to your 2 | # application. 3 | module Orion::DSL::WebSockets 4 | # Defines a websocket route to a callable object. 5 | # 6 | # You can route to any object that responds to `call` with a `HTTP::WebSocket` and an `HTTP::Server::Context`. 7 | # 8 | # ``` 9 | # router MyRouter do 10 | # ws "/path", Callable 11 | # end 12 | # 13 | # module Callable 14 | # def call(ws : HTTP::WebSocket, cxt : HTTP::Server::Context) 15 | # # ... do something 16 | # end 17 | # end 18 | # ``` 19 | macro ws(path, ws_callable, *, helper = nil) 20 | # Build the ws handlers 21 | %ws_handler = HTTP::WebSocketHandler.new do |websocket, context| 22 | {{ ws_callable }}.call(websocket, context.as(::Orion::Server::Context)) 23 | nil 24 | end 25 | 26 | # Build the proc 27 | %proc = -> (context : ::Orion::Server::Context) { 28 | %ws_handler.call(context) 29 | nil 30 | } 31 | 32 | # Define the action 33 | %action = ::Orion::Action.new( 34 | %proc, 35 | handlers: HANDLERS, 36 | constraints: CONSTRAINTS 37 | ) 38 | 39 | # Add the route to the tree 40 | %full_path = ::Orion::DSL.normalize_path(base_path: {{ BASE_PATH }}, path: {{ path }}) 41 | TREE.add(%full_path, %action) 42 | 43 | {% if helper %} # Define the helper 44 | define_helper(base_path: {{ BASE_PATH }}, path: {{ path }}, spec: {{ helper }}) 45 | {% end %} 46 | 47 | %action.constraints.unshift ::Orion::RequestMethodsConstraint.new("GET") 48 | %action.constraints.unshift ::Orion::WebSocketConstraint.new 49 | end 50 | 51 | # Defines a websocket route to a websocket compatible controller and action (short form). 52 | # You can route to a controller and action by passing the `to` argument in 53 | # the form of `"MyWebSocket#action"`. 54 | # 55 | # ``` 56 | # router WebApp do 57 | # ws "/path", to: "Sample#ws" 58 | # end 59 | # 60 | # class SampleController < WebApp::BaseController 61 | # def ws 62 | # # ... do something 63 | # end 64 | # end 65 | # ``` 66 | macro ws(path, *, to, helper = nil) 67 | {% parts = to.split("#") %} 68 | {% controller = run("../inflector/controllerize.cr", parts[0].id) %} 69 | {% action = parts[1] %} 70 | {% raise("`to` must be in the form `controller#action`") unless controller && action && parts.size == 2 %} 71 | ws({{ path }}, controller: {{ controller.id }}, action: {{ action.id }}, helper: {{ helper }}) 72 | end 73 | 74 | # Defines a match route to a controller and action (long form). 75 | # You can route to a controller and action by passing the `controller` and 76 | # `action` arguments, if action is omitted it will default to `match`. 77 | # 78 | # ``` 79 | # router WebApp do 80 | # ws "/path", controller: SampleController, action: ws 81 | # end 82 | # 83 | # class SampleController < WebApp::BaseController 84 | # def ws 85 | # # ... do something 86 | # end 87 | # end 88 | # ``` 89 | macro ws(path, *, action, controller = CONTROLLER, helper = nil) 90 | ws( 91 | {{ path }}, 92 | ->(websocket : HTTP::WebSocket, context : ::Orion::Server::Context) { 93 | {{ controller }}.new(context, websocket).{{ action }} 94 | }, 95 | helper: {{ helper }} 96 | ) 97 | end 98 | 99 | # Defines a match route with a block. 100 | # 101 | # Pass a block as the response and it will be evaluated as a controller 102 | # 0..2 block parameters are accepted. The block itself will always be evaluated 103 | # as a controller and have access to all controller methods and macros. 104 | # 105 | # ``` 106 | # router MyRouter do 107 | # ws "/path" do |websocket, context| 108 | # # ... do something 109 | # end 110 | # end 111 | # ``` 112 | macro ws(path, *, helper = nil, &block) 113 | {% controller_const = run "../inflector/random_const.cr", "Controller" %} 114 | struct {{ controller_const }} 115 | include ::Orion::Controller 116 | 117 | def handle 118 | {% if block.args.size == 0 %} 119 | {{ block.body }} 120 | {% elsif block.args.size == 1 %} 121 | action_block = ->({{ "#{block.args[0]} : HTTP::WebSocket".id }}){ 122 | {{ block.body }} 123 | } 124 | action_block.call(websocket) 125 | {% elsif block.args.size == 2 %} 126 | action_block = ->({{ "#{block.args[0]} : HTTP::WebSocket, #{block.args[1]} : HTTP::Request".id }}){ 127 | {{ block.body }} 128 | } 129 | action_block.call(websocket, request) 130 | {% else %} 131 | {% raise "block must have 0..2 arguments" %} 132 | {% end %} 133 | end 134 | end 135 | ws({{ path }}, controller: {{ controller_const }}, action: handle, helper: {{ helper }}) 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /src/orion/error_page.html.ecr: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= page_title %> 6 | 7 | 37 | 38 | 39 | 40 |
41 |
42 |

<%= message %>

43 |

<%= subtext %>

44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /src/orion/errors.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | macro exception(const) 3 | # :nodoc: 4 | class Orion::{{ const }} < Exception; end 5 | end 6 | 7 | exception DoubleRenderError 8 | exception RoutingError 9 | 10 | # :nodoc: 11 | class Orion::ParametersMissing < Exception 12 | def initialize(keys : Array(String)) 13 | initialize("Missing parameters: #{keys.join(", ")}") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/orion/exception_page.cr: -------------------------------------------------------------------------------- 1 | require "exception_page" 2 | 3 | class Orion::ExceptionPage < ::ExceptionPage 4 | def styles : Styles 5 | ExceptionPage::Styles.new( 6 | accent: "#2E1052", 7 | logo_uri: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA2NCA2NCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjI7Ij48ZyBpZD0iTGF5ZXIxIj48Zz48cmVjdCB4PSIxMS4wNDQiIHk9IjcuMTE3IiB3aWR0aD0iNDEuNDMyIiBoZWlnaHQ9IjMyLjczOSIgc3R5bGU9ImZpbGw6IzBkMGQwZDsiLz48L2c+PGc+PHBhdGggZD0iTTM3LjA0MywxOC41NzJjLTAuNjE3LDEuNDExIDAuODMyLDIuMjQ4IDEuMTYzLDQuNDk3YzAuNDMzLC0zLjAzOSAtMS4xNjMsLTQuNDk3IC0xLjE2MywtNC40OTciIHN0eWxlPSJmaWxsOiM5MTE3NDA7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zMy4yNjMsMTcuODkxYzAuNzE5LDAuMjcyIDIuMjk5LC0wLjYxMiAyLjY2NCwwLjA4NWMwLjM2NSwwLjY5NiAwLjc4NiwtMS4yODMgLTAuNTYzLC0xLjE4NWMtMS4zNDksMC4wOTggLTIuMTAxLDEuMSAtMi4xMDEsMS4xIiBzdHlsZT0iZmlsbDojOTExNzQwO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMzcuOTEyLDI1Ljk1MmMtMC40OTEsMS42NDUgLTIuNDIzLDEuMjQgLTIuMjkyLDEuOTljMC4xOTksMS4xNDMgLTAuMzU4LDIuMTY4IC0wLjg3NSwyLjMxM2MwLDAgMC40MzgsLTIuMDYgLTAuNTE3LC0yLjA5NmMwLjIzOSwwLjU3OCAwLjIzOSwxLjg3OSAtMS4wMzMsMi41MjRjLTEuMjQ0LDAuNjMxIC0zLjY1NywwLjkwOSAtNi41MTksLTAuMjQ3Yy0xLjIzMywtMC42MTUgLTMuNzIzLC0yLjk0OSAtMy42OTcsLTUuOTI4YzAuMzk3LDEuMTkzIDAuOTU0LDEuNjI2IDAuOTU0LDEuNjI2Yy0wLjIzOSwtMS4yMjkgLTAuODc1LC0zLjQ3IDAuMDc5LC00Ljk1MmMwLjk1NCwtMS40ODIgMi44MjMsLTAuNjg2IDMuMzc5LC0xLjIyOWMwLjU1NywtMC41NDIgLTEuMTI1LC0xLjg0MyAtMC45NzQsLTIuNjY5YzAuMjU4LC0xLjQwOSAzLjg3LC0yLjY3MiA0LjgyNCwtMS45MTNjMC45NTQsMC43NTkgMC4zMTYsMS42NDYgMC45OTIsMi4yMjVjLTAuMDgsLTAuNzIzIDAuNTk2LC0xLjMzOCAwLjE5OCwtMi4wMjVjMC43NTUsMC4zOTggLTAuMTE5LDEuNDgyIDAuNTk3LDIuMTY5YzAuNzE1LDAuNjg3IDEuOTg3LC0wLjYxNCAyLjU4MywtMC4yODljMC41OTcsMC4zMjUgMC4yNzksMi4xMzIgMC44NzUsMi44OTJjMCwtMC45NzYgMC41NTcsLTEuNzcyIDAuNTU3LC0xLjc3MmMtMC4xMTIsMS4zMjcgMS45ODUsMy42NDMgMC44NjksNy4zODFtLTAuNzkzLC0xMS4wODljMCwwIDAuNTEzLDEuMDg3IDAuMjc2LDIuMzJjLTAuMTQxLC0wLjcwNSAtMC40NTksLTEuMzY2IC0wLjkyLC0xLjk3Yy0wLjAxNiwtMC4wMTggLTAuMDMzLC0wLjAzOSAtMC4wNTIsLTAuMDY3Yy0wLjAyNSwtMC4wMzIgLTAuMDUxLC0wLjA2MyAtMC4wNzYsLTAuMDk1Yy0wLjQxOSwtMC40MTcgLTAuNjcyLDEuMDkzIC0xLjQzNSwxLjEyNWMtMC44MDgsMC4wMzQgLTAuNzU0LC0xLjg1OSAtMi45MjIsLTEuODk5YzIuOTM5LC0wLjkzMiAzLjAxNywxLjE1NSAzLjIxOSwwLjYzYzAuMjAyLC0wLjUyNCAtMC4yODEsLTEuMTU5IC0wLjk5NywtMS42MTljLTAuNDM2LC0wLjI1NCAtMC45MDYsLTAuNDg0IC0xLjQwNSwtMC42ODdjLTAuMjczLDIuMDQxIC0zLjYyMywtMC43ODkgLTcuNDUsMi42OTJjLTIuNDU0LC0wLjY4MyAtNC4zMjksMC43OTMgLTQuOTU4LDIuNjczYzEuODUzLDAuMzg3IDEuOTY0LDIuNjY1IDEuMDYsMi45OTNjLTAuOTA0LDAuMzI3IC0wLjc0LC0xLjA5NCAtMC44MDMsLTEuNzU5Yy0xLjY3OCwzLjUyNyAwLjg3NSw3Ljk4OCAzLjE5OSw5LjE4M2MtMy4yMDksLTEuMTE5IC0zLjM2NCwxLjgzIC00LjE5MywwLjcyNWMtMC4zMDcsMS43OCAwLjY5NCwyLjgyNSAxLjgyMywyLjg4Yy0wLjcxOCwxLjM4MSAwLjA1NSwzLjcwMSAyLjQ4Niw0LjY5NmMtMC4zODcsLTAuNTUzIC0xLjA0NywtMC45MjEgLTAuOTM5LC0yLjMyYzAuMDU1LDAuNjYzIDEuNDkxLDIuMTU0IDMuNjQ2LDIuNDg2YzAuNjc4LDAuMTA0IDEuNDQ5LDAuMjMgMi4yMDQsMC40MDNjLTEuMzM2LC0xLjI1MSAtMi4wMjMsLTIuNTY0IC0yLjg1NiwtMi41NjRjLTAuNjE0LDAgLTAuNTYsMC40OTEgMC4wOCwwLjk5M2MwLjY5OCwwLjU0NiAyLjAwOCwwLjk1NiAxLjI4OSwwLjg5NWMtMC44NzksLTAuMDc2IC0xLjUyMywtMC4yODkgLTIuMDAxLC0wLjU3NGMtMC41NjgsLTAuMzM3IC0wLjk2OCwtMS4zNDcgLTEuMDg4LC0yLjA5NWMtMC4wMDIsMC4yMTggLTAuMDM2LDAuNTk4IDAuMDMxLDAuOTk2Yy0wLjQzOSwtMC44MDUgLTAuNDMxLC0xLjYxNSAtMC43MTksLTEuNzE4Yy0wLjU2LC0wLjE5OSAtMS4wMzYsMS4xOTcgLTEuMzAxLDAuNjEzYy0wLjI2NiwtMC41ODQgMC43MzEsLTEuMjc3IDAuMzA2LC0xLjM4NGMtMC40MjUsLTAuMTA3IC0wLjkwNSwtMC45NTcgLTEuMTA3LC0xLjYxOGMwLjMyOSwwLjEzNyAwLjc5NiwxLjExMSAxLjcwNSwtMC4yMjZjMC42NCwtMC45NDMgMS43NDMsLTAuNTU3IDMuMTA2LDAuNDg4YzAuNzgzLDAuNjQ1IDIuNDYxLDIuNzQgMC41MzMsMi42MDhjMC41MzUsMC43OTMgMS4xNDUsMC40ODkgMS4xNDUsMC40ODljMC44MzQsMS42MyAyLjE1OCwxLjQ0IDMuODEsMi45MzljMC43MjQsMC42NTcgMC44ODksMS42NzQgMC42NjMsMi4yMDljMS4xMDUsLTEuNDEzIC0wLjM4NiwtMy41NTcgLTEuOTM2LC01LjE0N2MwLDAgMi4xMzEsMS43MzcgMy44MTUsMS40NzdjLTIuMjA2LC0wLjk1OSAtMi41NDIsLTIuNyAtNC43MzQsLTQuMTE3YzIuOTk2LDEuNDkyIDguOTQyLDEuODY4IDkuNTE3LDAuMTQ2YzAsMCAtMS40NTgsMC44MzUgLTIuNTc3LDAuMTNjMi41NzcsLTAuMTMgMy42MzYsLTEuNjEyIDMuOTIzLC00LjMwMmMwLjE2MywtMS41MzUgLTAuNjA1LC0yLjQ2MyAtMC4wMzMsLTMuMDQ1YzAuMjI1LC0wLjIyOSAwLjg3LC0wLjY1MiAxLjI1NSwwLjgxMWMxLjAwNSwtMy43OTIgLTMuMDQyLC05LjM0NCAtNC41ODksLTEwLjM5NCIgc3R5bGU9ImZpbGw6IzQ1MzE0YTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI0LjA1NiwyOC45MTJjLTEuMTg5LC0wLjQyNiAtMi4yMDQsLTAuNjczIC0zLjI3MSwwLjIwN2MwLjc4NCwtMC4wODcgLTAuMDA5LDAuNTUyIC0wLjcyNCwwLjU1MmMwLDAgLTAuMTQ1LDEuNDY4IDEuMTUxLDEuODEyYy0wLjU0NywtMC41NzEgLTAuNjE0LC0xLjQxNSAtMC4wMiwtMS45MTNjMC41OTUsLTAuNDk4IDEuNzg2LC0wLjIwMiAxLjQ4MywwLjYxMmMtMC4xMTcsMC4zMTUgLTAuNDY4LDAuNjEgLTAuNDY4LDAuNjFjMS4yNzIsMCAxLjA0NywtMS43MzUgMS44NDksLTEuODgiIHN0eWxlPSJmaWxsOiM1YzQ4NjY7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zMS4wMTksMzUuNjQ5Yy0wLjQ1OCwtMC45NDQgLTAuOTk1LC0xLjc2MSAtMi4wOTMsLTIuMzQ3Yy0xLjEyNSwwLjE3OCAxLjI1MiwxLjUzMSAyLjA5MywyLjM0NyIgc3R5bGU9ImZpbGw6IzI4MmExZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI5LjY1LDE2LjE0M2MwLjIzLDAuMjE4IDAuNTYsMC43OTkgMS4xOTIsMC41NTVjLTEuMDE5LC0wLjUzIC0xLjE5MiwtMC41NTUgLTEuMTkyLC0wLjU1NSIgc3R5bGU9ImZpbGw6I2ZmMDA1ZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTM0LjY3OCwyMC4wMDVjLTAuNzkxLC0wLjk3MiAtMi4xMjgsLTAuMDMgLTEuOTE2LDAuNzg4YzAuMjEzLDAuODE4IC0wLjgyLDEuMjQ1IC0xLjE1OSwwLjU2MmMtMC4xMTUsLTAuMzE4IC0wLjE2OSwtMC41NzYgLTAuMTI1LC0wLjc0MmMtMS4zOTgsLTAuMTYyIDAuNDY0LDMuNzczIC0wLjkyMywzLjUyYy0wLjg4MSwtMC4xNjIgMC4zOTksLTEuNTI4IC0wLjYwNiwtMi4wOTVjMC40MTUsMC45NTkgLTIuOTA1LDAuNDc4IC00LjM1MywyLjYwNWMtMS43NjIsMi41ODggMC4yNTEsNS43MTEgMS45NjIsNi4xMjNjNi41ODksMS41ODUgNi4zNjcsLTMuNjk3IDUuOTY4LC01LjE5NGMtMC4wMTEsLTAuMDQgLTAuMDIzLC0wLjA4MiAtMC4wMzYsLTAuMTI0YzAsMCAwLDAgMCwtMC4wMDFjLTAuMjYxLC0xLjkwOCAtMC43MjgsLTIuMjE5IC0wLjcyOCwtMi45NzljMCwtMC42NjggMC42NjksLTEuMTI1IDAuNjAzLC0wLjA5MmMtMC4wNjYsMS4wMzQgMC44ODcsMS4zMzggMS40OTUsMS4yMTZjLTAuOTEyLC0wLjM2NCAwLjYwOCwtMi42MTQgLTAuMTgyLC0zLjU4NyIgc3R5bGU9ImZpbGw6IzQ3MjgyZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI3LjIwOSwyMS4xMDljLTAuMjQ5LDAuNTI4IC0wLjY5NSwwLjU4MiAtMS4xMzksMS4yNDVjMC4wMzksLTAuMzQ4IC0wLjAyMywtMS4wODEgLTAuMzU5LC0xLjI0NWMtMC4wMjQsMS42MjIgLTIuNjc2LDEuNzM5IC0xLjAzNCw0Ljc0NWMtMC40MTMsLTIuNjA2IDMuMDM3LC0yLjg0OCAyLjUzMiwtNC43NDUiIHN0eWxlPSJmaWxsOiM0NzI4MmU7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yNy4yNiwxOC4yMTRjLTAuMjIzLC0wLjkwMSAwLjcyNiwtMS45ODYgMS45NDEsLTEuNTk2YzEuMTIxLDAuMzYxIDIuMjc3LDIuMTExIDAuNTI2LDQuMTE5YzAuNDA4LC0xLjE0NSAwLjM3LC0yLjUyOSAtMC40MjcsLTIuMTExYy0wLjc4NiwwLjQxMiAwLjY1NywyLjk4IC0wLjg5MywzLjU4OGMwLjU0OSwtMS4yMzYgLTAuMTYsLTIuMTI4IC0wLjcyNywtMS44OTJjMC4zNjEsLTAuNTg5IDAuNDI4LC0xLjIxNyAtMC40MiwtMi4xMDgiIHN0eWxlPSJmaWxsOiM0NzI4MmU7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zNS45ODEsMjAuOTc4Yy0xLjg0NiwxLjIzNiAtMC4xNzUsMy44NzkgLTAuNzM5LDQuMDQxYzAuNjQxLC0wLjUwMSAwLjY0MSwtMS4zMTcgMC42NDEsLTEuMzE3YzAuMzEzLDAuNDI4IC0wLjUyOSwzLjMyOCAtMS4xODMsMy44ODZjMS4yMzUsLTAuNDU4IDIuOTA4LC0xLjcwMiAyLjcxNSwtNC4xMDZjLTAuMDksLTEuMTEzIC0xLjAyMiwtMS43NDUgLTEuMDIyLC0xLjc0NWMwLjA4MSwwLjQzMiAtMC4wODYsMC43NDEgLTAuNDEyLDAuNjI0Yy0wLjMyNSwtMC4xMTcgLTAuMzY2LC0wLjc5MyAwLC0xLjM4MyIgc3R5bGU9ImZpbGw6IzQ3MjgyZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTMxLjUyNiwyNC43NjZjMC4zNjEsLTAuNDU3IDAuMjg3LC0xLjAyNSAtMC4xNDUsLTEuNjM5Yy0wLjI1MSwtMC4zNTcgLTAuMDM1LC0xLjIyIDAuMzg5LC0wLjY0M2MwLjUxNywwLjcwNCAwLjczOSwwLjAwNiAwLjEyNCwtMC43ODNjLTAuMzAxLC0wLjM4NyAtMC43NDEsLTEuMTQ3IC0wLjQ2MywtMS42MDljLTAuNjQ4LDAuNjExIDAuMDMxLDEuNTkyIC0wLjA4NSwxLjgxNWMtMC4xMTIsMC4yMTYgLTAuODA1LDAuNTAzIC0wLjQzMiwxLjEyMmMwLjQ0NywwLjc0IC0wLjU0MywxLjI0MSAtMC4yMTUsMS42NDFjMC4zMjgsMC4zOTkgMC44MjcsMC4wOTYgMC44MjcsMC4wOTYiIHN0eWxlPSJmaWxsOiM0NTMxNGE7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zMC4yMjEsMzEuMTkxYzIuMzUyLDAuNjY3IDMuOTUzLDEuMDc4IDQuNjkzLDAuODczYzAuNzQsLTAuMjA2IDAuNjMzLC0xLjE1MiAwLjYzMywtMS4xNTJjMCwwIDIuMzM0LDAuNzY0IDMuNjY3LC0xLjExMWMwLjk2NCwtMS4zNTcgMC41NTMsLTMuNjA4IDAuNTUzLC0zLjYwOGMtMS40NjgsMS4wNzUgLTAuODk0LC0yLjA2MyAwLjQ3OCwtMi40MzhjMC42NjgsLTAuMTgzIDEuMjgyLDAuMzc1IDEuMjgyLDAuMzc1YzAuMTY4LC0xLjY3NiAtMC44NDEsLTEuMzk5IC0wLjg0MSwtMS4zOTljMC40NDEsLTAuODcyIC0wLjAyNiwtMy4xMzkgLTEuNjgyLC0zLjEzOWMwLDAgMS44ODcsMC43MyAwLjQ4LDMuMTM5Yy0xLjMzOSwyLjI5MyAtMC40LDMuNzM4IC0yLjk1Myw1Ljk4MWMwLjgyNSwwLjE2MyAyLjAzMiwtMS4yNTUgMi4wMzIsLTEuMjU1YzAuMjQsMS43NDQgLTEuNDQyLDIuNTc4IC0zLjExNCwyLjkwM2MtMS44OTMsMC4zNjggLTUuMjI4LDAuODMxIC01LjIyOCwwLjgzMSIgc3R5bGU9ImZpbGw6IzVjNDg2NjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI0LjIwOSwyNy41ODhjLTEuMjY4LC0wLjkwNiAtMi44ODksLTIuMzQzIC0zLjEwNCwtMy44MzNjMC40NjksMC40NTUgMC43ODQsMC44ODggMC43ODQsMC44ODhjLTAuMzE1LC0wLjQzMyAtMC43NDYsLTEuNzggLTAuMzczLC0yLjYwNWMwLjM3MywtMC44MjUgMS40MjQsLTEuMjQ5IDAuODAxLC0zLjAzNGMwLjM2MSwwLjIxIDAuNjYxLDAuNDczIDAuNjYxLDAuNDczYzAsMCAwLC0xLjkzNCAtMS42NjgsLTEuOTM0YzEuMjEsLTEuMTE2IDIuNTUsLTEuNjk1IDMuNzQ3LC0xLjhjLTEuNTksMC44NDIgLTIuMDA1LDEuOTA1IC0xLjI1OCwxLjhjMC43NDcsLTAuMTA1IDEuODg4LC0yLjEwOCAyLjg3NiwtMS45OWMwLDAgLTAuOTI0LDAuNzk5IC0xLjE5OSwxLjY2N2MtMC40NDksMS40MTYgMC40NzIsMi4yMDUgMC45NDEsMi40MTVjLTAuODQ2LDAuMTA1IC0xLjcyOSwtMC4xMDMgLTIuNTk4LDAuNjg3Yy0xLjUwMiwxLjM2NCAtMS4wMTIsNC45MDYgMC4zOSw3LjI2NiIgc3R5bGU9ImZpbGw6IzVjNDg2NjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI0Ljg3MywyOS45NzNjLTEuMTAyLC0wLjQ4MiAtMS42ODUsMC44OTQgLTAuODkzLDEuMzI2YzEuMDI5LDAuNTYxIDEuOTc1LC0wLjg1NCAwLjg5MywtMS4zMjYiIHN0eWxlPSJmaWxsOiM5MTE3NDA7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yNS41NjgsMjkuMDMyYy0yLjAxNiwtMS4wNDEgLTEuMjQzLC00LjYwNSAtMi4zOCwtNC43OTFjMC41ODMsLTAuMjI3IDAuOTA0LDAuNDE4IDEuMDUxLDEuMzE1YzAuMDY3LDAuNDA3IDAuMDM2LC0wLjU1NiAwLjQ4LC0xLjI1OWMwLjMyNywtMC41MTYgMC44ODcsLTAuNzU5IDEuMjEyLC0wLjQxYy0xLjI4LDAuMTExIC0wLjk2NywzLjQyMiAtMC43MzEsMi41NDdjMC4yNSwtMC45MjYgMC43MiwtMC43ODIgMC42OTksLTEuMTU5YzAuMDQ3LDAuNjc3IC0xLjMzMiwxLjgyNyAtMC4zMzEsMy43NTciIHN0eWxlPSJmaWxsOiM5MTE3NDA7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zNi41MDEsMjQuNTE4Yy0xLjM4NCwwLjU3OCAtMC45NTYsMS45NTIgLTEuNDY1LDIuMjY4Yy0wLjUxLDAuMzE2IC0wLjk1OSwtMC45MTUgLTIuMjUyLC0wLjI2OWMxLjI2NCwwLjE3MSAxLjI5MywyLjIzMiAwLjk5OSwyLjQ4NGMtMC4yOTQsMC4yNTEgLTAuMTc2LC0wLjc4IC0wLjgyMiwtMC42NzFjMC4zNTIsMS4xMDggLTEuNTI4LDIuMTIzIC0xLjk0MiwxLjYyOGMtMC40MTMsLTAuNDk1IDAuMjUzLC0wLjcyNiAwLjQ1OSwtMi4wMTljMC4yMDYsLTEuMjkyIC0wLjYxNiwtMi40MDEgLTEuNjAzLC0yLjYxM2MwLDAgMC42NjcsMC41MjggMC40NDcsMC45NjhjLTAuMjU5LC0wLjQ3IC0xLjQ4MywtMS4wOTkgLTMuMDkyLC0wLjQwOGMtMi4xNzgsMC45MzYgLTEuNDI3LDMuNzMgLTAuNDMyLDQuMzMxYzMuNDUzLDIuMDg3IDguMjc4LDAuMDAxIDkuMzA3LC0xLjIxNmMxLjAyOSwtMS4yMTcgLTAuMDEyLC0zLjM2NSAwLjM5NiwtNC40ODMiIHN0eWxlPSJmaWxsOiM5MTE3NDA7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yNy4xMjgsMzAuMzUxYy0wLjQ4OCwtMC4yMDMgLTEuNDM5LC0wLjkyNyAtMS40NDEsLTEuODIzYy0wLjAwMSwtMC41NjYgMC4yNzUsLTAuOTQ0IDAuNTksLTEuMTExYy0wLjE3OCwwLjI0MSAtMC4yNTMsMC44IDAuMDIxLDAuNzgzYzAuMzY1LC0wLjAyMyAwLjA4OCwtMC45MzggMC44NzMsLTEuNTA2YzAuNDk5LC0wLjM1OSAxLjQxOSwtMC4xOTYgMS43MjUsMC4wMjJjMC4zMDUsMC4wNjYgLTAuMDIyLC0wLjUyNCAtMC4wMjIsLTAuNTI0YzEuMzIyLDAuNjYyIDEuODcxLDIuNDExIDEuMDg1LDMuOTVjLTAuMTAzLC0wLjg2NiAtMC4xMjIsLTAuMDc5IC0wLjczNiwwLjIwOWMtMC42MjIsMC4yOTMgLTEuMzk3LDAuMjkzIC0yLjA5NSwwIiBzdHlsZT0iZmlsbDojZmYwMDVlO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjkuMiwyOC40OThjLTAuMDA1LDAuMDkzIC0wLjA2MywwLjIwMiAtMC4xNDQsMC4zMjVjLTAuMzA2LC0wLjkzMiAtMS4yODQsLTAuNzYzIC0xLjQ0NCwtMS4xNjJjLTAuMjc3LDAuMjY0IC0xLjAxLDAuODE5IC0xLjEyNSwxLjUwNWMtMC4xMTYsMC42OSAwLjQ4MSwxLjIzNSAwLjk5MSwxLjMyMWMwLjMxMiwwLjA4IDAuNjgzLDAuMDMyIDAuOTk2LC0wLjEwOWMwLjAwOSwwLjA0OSAwLjAxOSwwLjA5OCAwLjAzNCwwLjE0OGMxLjExNCwtMC4zOTEgMC44ODMsLTEuNjU5IDAuNjkyLC0yLjAyOCIgc3R5bGU9ImZpbGw6I2VkZThhZDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTM2LjUzLDI3LjEzYzAuODAxLC0wLjU5MiAxLjQxOCwtMS41MzggMS4zNzksLTIuNDg3YzAuNTcsMC44MjggMC44MDIsMi4yNjYgLTEuMzc5LDIuNDg3IiBzdHlsZT0iZmlsbDojOTExNzQwO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjMuODE5LDIwLjMyMmMtMS4xNiwwLjYwMyAtMS44MDksMS43MTYgLTAuMzcxLDMuNDMzYy0wLjI5MSwtMS4zOTIgLTAuMjI3LC0yLjY5MSAwLjM3MSwtMy40MzMiIHN0eWxlPSJmaWxsOiM5MTE3NDA7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zMC4yMjEsMjYuODczYzAuMjE5LDAuMzU0IDAuMzg4LDAuNzMxIDAuMzUzLDEuNDYyYzAuMjkyLC0wLjQ3NSAwLjQyMSwtMS4wNjcgLTAuMzUzLC0xLjQ2MiIgc3R5bGU9ImZpbGw6I2ZmMDA1ZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTM2LjEwNSwyOS4wMDFjMC45OTYsLTIgMC41NywtMi45NTMgMC45OTYsLTMuNDkzYy0wLjg3NywwLjM0NiAtMS4zMjEsMi42MDIgLTEuNzM3LDMuNTIxYy0wLjQxMiwwLjkwOCAtMS4yMjgsMS4xMDMgLTEuNDczLDEuMTEyYy0wLjYyOCwwLjAyNCAtMC41MSwtMC41NzEgLTAuOTc2LC0wLjU4OWMwLjM5NywwLjYxMSAtMC42NDcsMS4yMDUgLTIuMjYyLDEuMjUzYy0wLjk4MiwwLjAyOSAtMS45MjMsMC4wMjEgLTMuMTM0LC0wLjUxM2MtMi45MjIsLTEuMjkxIC00LjQ2LC0yLjgxMiAtNC41MDcsLTUuMjk4Yy0wLjAzOSwtMi4wOTkgMS41MDQsLTMuMTEyIDEuNTA0LC0zLjExMmMtMC43NzEsLTAuMTc1IC0xLjM0LDEuMjQxIC0xLjM0LDEuMjQxYzAuMjcyLC0xLjI0MSAxLjQ0LC0yLjQ3NCAwLjYyOCwtMi43MThjMC4yOTksMC41MDcgLTAuNDcsMC43NDkgLTEuMDI1LDEuNzE0Yy0wLjY2MSwxLjE0OCAtMC4yNTMsMi41MjYgLTAuMjUzLDIuNTI2Yy0wLjE4MywtMC42ODMgLTAuNjcxLC0wLjg0NyAtMS4wMDIsLTAuODQ3YzAuMzA1LDAuMjQ1IDAuNDI4LDEuMTIxIDEuMjU1LDIuNTgyYzAuODEyLDEuNDM1IDEuOTk5LDIuNjkyIDEuNjUxLDIuNjIxYy0xLjQ5LC0wLjMwMiAtMS4zMywxLjU2IC0wLjM3NSwyLjMxMmMtMC4xNjYsLTAuNjcyIC0wLjIzNiwtMS40OTUgMS41NTcsLTEuMzY0YzAsMCAtMC40ODcsMC4xMzYgLTAuNDU4LDAuNjEyYzAuNDQ4LC0wLjQzMSAxLjQ3NiwtMC4xOTIgMS43NTUsMGMwLjUzMywwLjM2OSAwLjU1OCwxLjAyMiAwLjk5NSwxLjg2M2MwLjQ3NCwwLjkxNCAxLjI3LDEuMjU2IDEuODAyLDEuMzc1Yy0wLjQ4NiwtMC4zNyAtMC43MzQsLTEuMDY2IC0wLjM3MiwtMS4yNTNjMC4zMTgsLTAuMTYzIDAuNDg2LDAuNDU5IDAuODYxLDAuNzAzYy0wLjI0NCwtMC4zOTcgMCwtMC44MjUgLTAuNjQxLC0xLjQwNWMtMC4xOTIsLTAuMTc0IC0wLjMwMywtMC4zMTQgLTAuMzI3LC0wLjQyM2MwLjE3NywtMC40NDMgMy4yNzQsMC4xOTggNS43NCwtMC44NzZjMS43NDEsLTAuNzU5IDEuNzU5LC0yLjI3NiAyLjUyMiwtMi42MjFjLTAuMjYzLDAuMDIxIC0xLjAxMywwLjMzMSAtMS4zODQsMS4wNzciIHN0eWxlPSJmaWxsOiM3ZDY2ODc7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yNC40ODgsMzEuMjc0Yy0wLjM2NywtMC4wMzEgLTAuMTMyLC0xLjA1MSAwLjI4OSwtMC45OTNjMC40MiwwLjA1OCAwLjgxMywxLjA4NSAtMC4yODksMC45OTMiIHN0eWxlPSJmaWxsOiNmZjAwNWU7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yMy42NDUsMjUuNjczYzAuMDA0LDAuOTE0IDAuNzc4LDIuNTMzIDEuODMxLDMuNDg3Yy0xLjA3OCwtMC43MjYgLTEuNjA2LC0wLjYwOCAtMS42MDYsLTEuMTc5YzAuMTA4LDAuMjc0IDAuMzg3LDAuMjc5IDAuMzY0LDAuMDczYy0wLjA0NCwtMC40MTMgLTAuNjg4LC0wLjU5NyAtMC43NTEsLTEuMTgxYy0wLjA1LC0wLjQ1OSAwLjE1NywtMC4zNzUgMC4xNjIsLTEuMiIgc3R5bGU9ImZpbGw6I2I3YWRkNjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTM0LjkxNCwyOS42NTljLTAuMDUsMC4xNTMgMC42ODEsMC4zODEgMC45MDIsLTAuMzM4bC0wLjkwMiwwLjMzOFoiIHN0eWxlPSJmaWxsOiM0NTMxNGE7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yOS4yNzksMzAuNzk3YzAuNDQ3LDAuMTQ1IDEuODk4LC0wLjM4OCAyLjE5OSwtMC40MzdjLTAuMzk5LDAuMzkyIDAuNTE5LDAuNTcyIDEuMDAzLC0wLjE1OWMwLjQ0NiwwLjUxNiAwLjU3MiwwLjA4MyAxLjE0NSwwLjM0M2MtMC45NTUsLTAuMDg0IC0xLjE1NiwxLjAxIC00LjM0NywwLjI1MyIgc3R5bGU9ImZpbGw6I2I3YWRkNjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTM0LjcwMSwyOS41NjZjMC4xODEsMC4xMjEgMC41MzYsMC4yMTQgMC44OSwtMC4xMDRjMC41MTQsLTAuNDYxIDAuNDM4LC0xLjMzNiAwLjk0LC0xLjg3NGMtMC41NTQsMC4zMTEgLTAuOTA3LDEuMzk5IC0wLjk4NCwxLjU4YzAsLTAuMjA3IC0wLjAxNywtMC40ODQgMC4wNTIsLTAuNjY1Yy0wLjE4MSwwLjE1NSAtMC4xNzcsMC45NTkgLTAuODk4LDEuMDYzIiBzdHlsZT0iZmlsbDojYjdhZGQ2O2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjUuNzE5LDI5LjM4NmMtMC41MTksLTAuNzA5IC0xLjU5OSwtMC41MjcgLTIuNDk4LC0xLjQwNWMwLjEzMSwwLjIyIDAuNDc3LDAuNTE1IDAuODY3LDAuNzkzYy0wLjMyOSwtMC4xMzcgLTAuNjc4LC0wLjIzNCAtMS4wMTEsLTAuMjM0YzAuNjQxLDAuMjk5IDEuNTIsMC42NjEgMS41MiwwLjY2MWMtMC40NzcsLTAuMDE4IC0xLjEyMiwwLjYxNSAtMi4wODgsMC42NjJjMi4xMTYsMC4yNyAyLjk2NywtMC4yODEgMi45NjcsLTAuMjgxYy0xLjEwNSwtMC42OTUgLTAuNjE3LC0wLjc2NCAwLjI0MywtMC4xOTYiIHN0eWxlPSJmaWxsOiNiN2FkZDY7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yMy4xMjEsMjUuNTIyYy0wLjEyMSwtMC40NDggLTAuMjQ0LC0xLjEzNiAtMC4wNjksLTEuNzYxYzAuMjQxLC0wLjg2MSAwLjM0MywtMS41MzUgMC4xODcsLTEuODc5YzAuMTE0LDAuNzEgLTAuNDEyLDEuMjU4IC0wLjUwMywxLjkzNGMwLjA5MSwtMC42NzYgMC4wMjEsLTAuNjI2IDAuMDk2LC0xLjA1OWMtMC4zNjQsMC41OTggLTAuNDA2LDEuNjQ1IDAuMjg5LDIuNzY1IiBzdHlsZT0iZmlsbDojYjdhZGQ2O2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjQuMzgsMjQuMzM1Yy0wLjA5LC0wLjM1MiAtMC4xNDQsLTEuMjAyIDAuNTkzLC0xLjI2NmMtMC4yMzUsMC40NiAtMC4zNzYsMC41MTggLTAuNTkzLDEuMjY2IiBzdHlsZT0iZmlsbDojOTExNzQwO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjUuNTg0LDIzLjE4NmMtMC4yNzcsLTAuNTAxIC0wLjI1OCwtMS40NjMgMC4yMzcsLTEuODgzYzEuMDA5LC0wLjg1NiAyLC0wLjU1NCAyLjY5MywtMS4zMjFjLTAuMjkyLDAuNzY1IC0xLjk2NiwxLjUwNiAtMi4yMzgsMS42NTFjMC4yMDksLTAuMDY1IDAuMzk5LDAuMTM1IDAuMzQsMC40MDVjLTAuMjUyLC0wLjM4NiAtMS4xNTcsMC4yMzggLTEuMDMyLDEuMTQ4IiBzdHlsZT0iZmlsbDojOTExNzQwO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjUuMDc0LDIyLjQ2NmMtMC4wMjQsLTAuMzMzIDAuMjE0LC0wLjg5OCAwLjc0NywtMC42NTFjLTAuMjc5LDAuMTI3IC0wLjY1NiwwLjIyMyAtMC43NDcsMC42NTEiIHN0eWxlPSJmaWxsOiNmZjAwNWU7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zMS42MzcsMjQuMzgyYzAsMC4yODMgLTAuMjMsMC41MTQgLTAuNTE0LDAuNTE0Yy0wLjI4NCwwIC0wLjUxNCwtMC4yMzEgLTAuNTE0LC0wLjUxNGMwLC0wLjI4NCAwLjIzLC0wLjUxNSAwLjUxNCwtMC41MTVjMC4yODQsMCAwLjUxNCwwLjIzMSAwLjUxNCwwLjUxNSIgc3R5bGU9ImZpbGw6IzVjNDg2NjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI2LjA1LDMwLjc2OGMtMC41NjEsMCAtMS4xNTgsLTAuMTM5IC0xLjQ2MSwwLjc1MWMwLjM3OCwtMC4yOTIgMS4xNzUsLTAuMjUxIDEuNDYxLC0wLjc1MSIgc3R5bGU9ImZpbGw6IzdkNjY4NztmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI4LjAxMywxNi40MTRjMS4yMzMsLTAuNzgzIDEuMzAzLDAuMzggMS4wMzYsMC43NDdjMC42OTQsLTAuMzczIDAuNTk3LC0xLjA5OSAwLjM3LC0xLjIxNWMwLjIyNywwLjExNiAwLjc3LDAuOTQ5IDEuNTQsMC43MjZjLTAuMzQ0LC0wLjE3MyAtMS4zNDQsLTEuNjE3IC0yLjk0NiwtMC4yNTgiIHN0eWxlPSJmaWxsOiM0NTMxNGE7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zNS44NzgsMjUuOTljLTAuNzc1LDAuNjMyIC0wLjUxNCwxLjQ1OCAtMC43OTUsMi4zNDVjMC41NzQsLTAuNjUgMC42NTUsLTEuNTkxIDAuNzk1LC0yLjM0NSIgc3R5bGU9ImZpbGw6IzdkNjY4NztmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI0LjY1MSwxOS4xMTRjMC4yMTYsLTAuMjI3IDAuODI1LC0yLjY2NSAyLjAyNCwtMy41NjFjLTAuMjYzLDAuNTA4IC0wLjY1NywyLjc4MyAtMi4wMjQsMy41NjEiIHN0eWxlPSJmaWxsOiM0NTMxNGE7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zMC44MDIsMjEuMTA5Yy0wLjY2MSwtMC43NDggLTAuMTIzLC0xLjE0IC0wLjI3NywtMS43MDZjMC40NTUsMC41OCAwLjEyLDEuMzUxIDAuMjc3LDEuNzA2IiBzdHlsZT0iZmlsbDojNDUzMTRhO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMzUuNDQ4LDI5LjM2N2MwLDAuMTM3IC0wLjExMSwwLjI0OSAtMC4yNDgsMC4yNDljLTAuMTM3LDAgLTAuMjQ4LC0wLjExMiAtMC4yNDgsLTAuMjQ5YzAsLTAuMTM3IDAuMTExLC0wLjI0OCAwLjI0OCwtMC4yNDhjMC4xMzcsMCAwLjI0OCwwLjExMSAwLjI0OCwwLjI0OCIgc3R5bGU9ImZpbGw6I2VkZThhZDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI0LjY4LDMxLjE4M2MtMC4yMzgsLTAuMTU5IDAuMDk2LC0wLjM1NCAwLjIyOCwtMC41NWMwLjI5NiwwLjIwMiAwLjExNiwwLjYxOCAtMC4yMjgsMC41NSIgc3R5bGU9ImZpbGw6I2VkZThhZDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI5LjI0NSwyNy43NDFjMCwwLjEwOCAtMC4wODgsMC4xOTYgLTAuMTk2LDAuMTk2Yy0wLjEwOSwwIC0wLjE5NywtMC4wODggLTAuMTk3LC0wLjE5NmMwLC0wLjEwOCAwLjA4OCwtMC4xOTYgMC4xOTcsLTAuMTk2YzAuMTA4LDAgMC4xOTYsMC4wODggMC4xOTYsMC4xOTYiIHN0eWxlPSJmaWxsOiNlZGU4YWQ7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yOC40NSwyNi41MjJjMCwwLjEyMyAtMC4xLDAuMjIzIC0wLjIyMywwLjIyM2MtMC4xMjMsMCAtMC4yMjIsLTAuMSAtMC4yMjIsLTAuMjIzYzAsLTAuMTIzIDAuMDk5LC0wLjIyMiAwLjIyMiwtMC4yMjJjMC4xMjMsMCAwLjIyMywwLjA5OSAwLjIyMywwLjIyMiIgc3R5bGU9ImZpbGw6I2VkZThhZDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTMzLjI2MywyNy4zNGMwLDAuMDg4IC0wLjA3MiwwLjE1OSAtMC4xNTksMC4xNTljLTAuMDg5LDAgLTAuMTYsLTAuMDcxIC0wLjE2LC0wLjE1OWMwLC0wLjA4OCAwLjA3MSwtMC4xNiAwLjE2LC0wLjE2YzAuMDg3LDAgMC4xNTksMC4wNzIgMC4xNTksMC4xNiIgc3R5bGU9ImZpbGw6I2VkZThhZDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTMyLjYwNiwyNi44NzNjMC4yOTQsLTAuMDc0IDAuOTA1LDAuMDkyIDEuMTM5LDAuNDY3Yy0wLjI4MSwtMC43MTIgLTAuODg3LC0xLjA0IC0xLC0xLjgxOWMtMC4yNTQsMC42NzIgMC4xNzYsMS4xOCAwLjE3NiwxLjE4Yy0wLjMwOCwtMC4xMDMgLTAuMzE1LDAuMTcyIC0wLjMxNSwwLjE3MiIgc3R5bGU9ImZpbGw6I2ZmMDA1ZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI1LjE5MSwyNy4zYy0wLjIzOSwwLjYzOCAtMC4wNzQsMS4yMTkgMC4xNjYsMS41N2MwLjQxOCwwLjYxNSAwLjcyOSwwLjY4NyAwLjg0LDAuODU3YzAuMTksMC4yOSAtMC4zOTMsLTAuMjEyIC0wLjcwNSwwLjE2N2MwLDAgMC42OTgsMC4wMDkgMS43NzYsMC4zNmMwLjgyMiwwLjI2OSAxLjMyMiwwLjU2MSAxLjg4MSwwLjYyN2MxLjE3MywwLjEzOCAxLjE0NywtMC4xOSAyLjMyOSwtMC4zMTRjMCwwIC0wLjYxNiwtMC4zMSAtMS4xNTYsLTAuMDY4Yy0wLjU2NCwwLjI1MyAtMC41MjYsLTAuMTQ1IDAsLTAuMTgxYy0wLjM2MywtMC4xNzYgLTAuMzI0LDAuMDIzIC0xLjI2NywwLjA2OWMtMC44MjgsMC4wNCAtMS4wMjEsLTAuMDU5IC0xLjY1NCwtMC4zMTJjLTAuNjM0LC0wLjI1MyAtMi4wNTIsLTAuOTgyIC0xLjU3NywtMS42MjdjLTAuMzEsLTAuMTExIC0wLjQ2NCwwLjUzOSAtMC40MzUsMC4wNzFjMC4wNCwtMC42MzYgLTAuMTk1LC0wLjUzNSAtMC4xOTgsLTEuMjE5IiBzdHlsZT0iZmlsbDojZWRlOGFkO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjcuNTY5LDI5Ljg3M2MwLjQ5OSwtMC4yNTcgMC43MTUsMC43NjIgMS4zMDIsMC45NTZjMCwwIDAuMTY4LDIuMTc1IC0wLjM2NywxLjM4NGMtMC40OTUsLTAuNzMxIC0xLjUyMiwtMS44NTMgLTAuOTMyLC0xLjg1M2MwLjMyNywwIDAuMTM2LC0wLjQ1NiAtMC4wMDMsLTAuNDg3IiBzdHlsZT0iZmlsbDojN2Q2Njg3O2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjcuMDE1LDMwLjQ2N2MxLjI2MSwwLjMzIDEuNzMsMC45NjUgMi4xNDYsMi4wMzFjLTEuMzUsLTAuMjYzIC0xLjQ5NywtMS40MzkgLTIuMTQ2LC0yLjAzMSIgc3R5bGU9ImZpbGw6IzQ1MzE0YTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI3Ljc4OSwzMC45MTRjMC4zNTYsLTAuMDIxIDAuOTYsMC40NjcgMS4wMTMsMC44MzVjLTEuMTU5LC0wLjA0IC0xLjI4OCwtMC44MTggLTEuMDEzLC0wLjgzNSIgc3R5bGU9ImZpbGw6I2ZmMDA1ZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI4LjM2LDMxLjQzOWMtMC4zMiwwIC0wLjM3NSwtMC40MTggLTAuMzc1LC0wLjQxOGMwLjQ2MSwwLjA2OSAwLjY0LDAuNDE4IDAuMzc1LDAuNDE4IiBzdHlsZT0iZmlsbDojZWRlOGFkO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjcuMjYsMzAuMzAzYzEuMzIxLDAuMjQ5IDEuODU3LDAuOTQxIDIuNDI3LDEuNTcxYy0wLjYsLTAuMzI2IC0wLjgyNywtMC41NzkgLTAuODI3LC0wLjU3OWMwLjQ3MSwwLjU0OSAwLjgwMywxLjM3MyAxLjI1LDEuNjZjMCwwIC0xLjAzMiwtMC42NjIgLTEuNTQ1LC0xLjQyNmMtMC42ODIsLTEuMDE2IC0xLjMwNSwtMS4yMjYgLTEuMzA1LC0xLjIyNiIgc3R5bGU9ImZpbGw6I2I3YWRkNjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI1Ljg1MiwzMS4wNjJjMCwwLjA4MiAtMC4wNjcsMC4xNDkgLTAuMTQ5LDAuMTQ5Yy0wLjA4MiwwIC0wLjE0OSwtMC4wNjcgLTAuMTQ5LC0wLjE0OWMwLC0wLjA4MiAwLjA2NywtMC4xNDkgMC4xNDksLTAuMTQ5YzAuMDgyLDAgMC4xNDksMC4wNjcgMC4xNDksMC4xNDkiIHN0eWxlPSJmaWxsOiNlZGU4YWQ7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yOC41NTYsMzIuNzQ5YzAsMC4xMzkgLTAuMTEyLDAuMjUyIC0wLjI1MSwwLjI1MmMtMC4xMzksMCAtMC4yNTEsLTAuMTEzIC0wLjI1MSwtMC4yNTJjMCwtMC4xMzggMC4xMTIsLTAuMjUxIDAuMjUxLC0wLjI1MWMwLjEzOSwwIDAuMjUxLDAuMTEzIDAuMjUxLDAuMjUxIiBzdHlsZT0iZmlsbDojZWRlOGFkO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjguMDU2LDMxLjk5N2MwLDAuMDcxIC0wLjA1NywwLjEyOCAtMC4xMjgsMC4xMjhjLTAuMDcsMCAtMC4xMjcsLTAuMDU3IC0wLjEyNywtMC4xMjhjMCwtMC4wNyAwLjA1NywtMC4xMjcgMC4xMjcsLTAuMTI3YzAuMDcxLDAgMC4xMjgsMC4wNTcgMC4xMjgsMC4xMjciIHN0eWxlPSJmaWxsOiNlZGU4YWQ7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zMi4zOSwyNC4yODRjLTAuOTMxLDAgLTEuMjQyLDAuMzEgLTEuMjQyLDEuMjQxYzAsLTAuOTMxIC0wLjMxLC0xLjI0MSAtMS4yNDEsLTEuMjQxYzAuOTMxLDAgMS4yNDEsLTAuMzExIDEuMjQxLC0xLjI0MmMwLDAuOTMxIDAuMzExLDEuMjQyIDEuMjQyLDEuMjQyIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMzQuNzk2LDI4LjMzNWMwLjA0NSwwLjI0MiAwLDAuNTE4IC0wLjMyNywwLjY1OWMwLjE1MywtMC4yMDIgMC4yODYsLTAuNTE1IDAuMzI3LC0wLjY1OSIgc3R5bGU9ImZpbGw6I2ZmMDA1ZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI0LjMyOSwyMC4zMjJjMC4wODcsLTAuMjg0IDAuNjQxLC0wLjI2NCAwLjc0NCwtMC44NTVjLTAuMjI0LDAuNDI3IC0wLjc5OSwwLjA5MSAtMC43NDQsMC44NTUiIHN0eWxlPSJmaWxsOiM5MTE3NDA7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0yOS43ODQsMzEuODI2YzAuODM4LC0wLjE5NyAxLjE1NCwxLjA1MSAxLjMzOSwxLjMwM2MtMC40NTEsLTAuMjY5IC0wLjkwMiwtMS4wODggLTEuMzM5LC0xLjMwMyIgc3R5bGU9ImZpbGw6IzdkNjY4NztmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTIxLjUyNCwyOS41N2MtMC41NzgsMC4xMDkgLTAuNjk1LDAuNzczIC0wLjQ4NywxLjI5N2MtMC4wMiwtMC42MDQgMC41MjQsLTAuNzg0IDAuNDg3LC0xLjI5NyIgc3R5bGU9ImZpbGw6IzI4MmExZTtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTI5LjA1MywxOS4xMTRjMC4yMjMsLTAuNzU0IDAuODI5LC0wLjk5NSAxLjE0MiwtMC4yMThjLTAuNDYyLC0wLjIwMyAtMC43NzcsLTAuNjAzIC0xLjE0MiwwLjIxOCIgc3R5bGU9ImZpbGw6IzkxMTc0MDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTQzLjgwNSwyMi42MThjMCwwLjQ0OSAtMC4zNjQsMC44MTMgLTAuODEzLDAuODEzYy0wLjQ0OSwwIC0wLjgxMywtMC4zNjQgLTAuODEzLC0wLjgxM2MwLC0wLjQ0OSAwLjM2NCwtMC44MTMgMC44MTMsLTAuODEzYzAuNDQ5LDAgMC44MTMsMC4zNjQgMC44MTMsMC44MTMiIHN0eWxlPSJmaWxsOiNkY2RjZGM7ZmlsbC1vcGFjaXR5OjAuNDQ7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjxnPjxwYXRoIGQ9Ik0zNi44NzgsMzEuOTk3YzAsMC40NDkgLTAuMzY0LDAuODEzIC0wLjgxMywwLjgxM2MtMC40NDksMCAtMC44MTMsLTAuMzY0IC0wLjgxMywtMC44MTNjMCwtMC40NDkgMC4zNjQsLTAuODEzIDAuODEzLC0wLjgxM2MwLjQ0OSwwIDAuODEzLDAuMzY0IDAuODEzLDAuODEzIiBzdHlsZT0iZmlsbDojNWM0ODY2O2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNNDQuNjU3LDIyLjU1NWMtMS4yOTcsMCAtMS43MjksMC40MzIgLTEuNzI5LDEuNzI5YzAsLTEuMjk3IC0wLjQzMywtMS43MjkgLTEuNzI5LC0xLjcyOWMxLjI5NiwwIDEuNzI5LC0wLjQzMyAxLjcyOSwtMS43MjljMCwxLjI5NiAwLjQzMiwxLjcyOSAxLjcyOSwxLjcyOSIgc3R5bGU9ImZpbGw6I2ZmZjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTM3LjU4NywzMS45OTZjLTEuMTM2LDAgLTEuNTE1LDAuMzc5IC0xLjUxNSwxLjUxNWMwLC0xLjEzNiAtMC4zNzgsLTEuNTE1IC0xLjUxNCwtMS41MTVjMS4xMzYsMCAxLjUxNCwtMC4zNzkgMS41MTQsLTEuNTE0YzAsMS4xMzUgMC4zNzksMS41MTQgMS41MTUsMS41MTQiIHN0eWxlPSJmaWxsOiNmZmY7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjwvZz48ZyBpZD0iUG9ydGhvbGUiPjxnPjxnPjxnPjxwYXRoIGQ9Ik01Ni4wMDIsOS4yNDNsMCwtNS4yMzNsLTQ4LjAwMywwbC0wLjAxMywwYzAsMCAtMC4xMjcsMC4wMDYgLTAuMzQ5LDAuMDE2Yy0wLjIyMSwwLjAwOSAtMC41NDEsMC4wNTQgLTAuOTE1LDAuMTQ1Yy0wLjc0OCwwLjE4MiAtMS43MTIsMC42NDEgLTIuNDMzLDEuMzg1Yy0wLjc0MywwLjcyIC0xLjIxNCwxLjY4NyAtMS40MDksMi40NDZjLTAuMDk5LDAuMzc5IC0wLjE0OSwwLjcwNiAtMC4xNjQsMC45MzNjLTAuMDE1LDAuMjI4IC0wLjAyMywwLjM1OCAtMC4wMjMsMC4zNThjMCwwIDAuMDAzLDAuMTQ0IDAuMDA1LDAuMjg4YzAuMDA2LDAuMTQzIDAuMDEyLDAuMjg3IDAuMDEyLDAuMjg3bDAsMC4wMDhsMi41MzYsMzEuMDgxbDAuMDAxLDAuMDEzYzAsMCAwLjAxNiwwLjEyOSAwLjA0NCwwLjM1NGMwLjAzMSwwLjIyNCAwLjA5MiwwLjU0NyAwLjIxNCwwLjkxOGMwLjExMywwLjM3NSAwLjMwNCwwLjc5MiAwLjU1MSwxLjIxMmMwLjI1NiwwLjQxNCAwLjU4LDAuODIgMC45NTQsMS4xNzRjMC4zNzcsMC4zNSAwLjgwMiwwLjY0NSAxLjIyOCwwLjg3YzAuNDMxLDAuMjE0IDAuODU0LDAuMzc1IDEuMjMsMC40NTdjMC4zNzQsMC4wOTIgMC42OTQsMC4xMjkgMC45MTYsMC4xNGMwLjIyMiwwLjAwOSAwLjM0OSwwLjAxNSAwLjM0OSwwLjAxNWw0Mi41MjcsMGwwLjAxMiwtMC4wMDFjMCwwIDAuMTI3LC0wLjAwNSAwLjM1LC0wLjAxNGMwLjIyMSwtMC4wMTIgMC41NDEsLTAuMDQ5IDAuOTE1LC0wLjE0MWMwLjM3NiwtMC4wODMgMC43OTksLTAuMjQ0IDEuMjMsLTAuNDU4YzAuNDI1LC0wLjIyNiAwLjg1LC0wLjUyMSAxLjIyNywtMC44NzJjMC4zNzQsLTAuMzU1IDAuNjk3LC0wLjc2MSAwLjk1MywtMS4xNzVjMC4yNDYsLTAuNDIgMC40MzgsLTAuODM3IDAuNTUsLTEuMjEyYzAuMTIxLC0wLjM3MiAwLjE4MywtMC42OTQgMC4yMTMsLTAuOTE4YzAuMDI4LC0wLjIyNSAwLjA0MywtMC4zNTQgMC4wNDMsLTAuMzU0bDAuMDAyLC0wLjAxMmwyLjUzNSwtMzEuMDhsMCwtMC4wMWMwLDAgMC4wMDYsLTAuMTQ0IDAuMDEyLC0wLjI4OGwwLjAwNSwtMC4yODdjMCwwIC0wLjAwNiwtMC4xNzQgLTAuMDM4LC0wLjUyMWMtMC4wNDcsLTAuMzQ0IC0wLjEyOCwtMC44NzIgLTAuNDA3LC0xLjUxM2MtMC4yNzQsLTAuNjM1IC0wLjczNywtMS4zOTYgLTEuNTM5LC0yLjA1Yy0wLjc4MSwtMC42NTggLTEuOTU4LC0xLjE4OCAtMy4zMjEsLTEuMTk0bDAsNS4yMzNjMC4wODgsMC4wMDUgMC4xNDMsMC4wNjcgMC4xNTksMC4xMWMwLjAyMSwwLjAzNSAwLjAwOCwwLjA1OSAwLjAxNSwwLjA1OWMtMC4wMDQsMCAtMC4wMDUsMC4wMjMgLTAuMDAxLDAuMDIzbDAsMC4wMDRsLTIuNTM1LDMxLjA4bDAsMC4wMDNjLTAuMDA4LDAuMDAyIC0wLjAwNiwwLjE0OSAtMC4xMjIsMC4yNTJjLTAuMTEsMC4xMDkgLTAuMjU0LDAuMDk1IC0wLjI1NiwwLjEwMmwtMC4wMDIsMC4wMDFsLTQyLjUyMSwwYy0wLjAwMiwtMC4wMDkgLTAuMTQ3LDAuMDA3IC0wLjI1NywtMC4xMDNjLTAuMTE1LC0wLjEwMiAtMC4xMTQsLTAuMjUgLTAuMTIxLC0wLjI1MmwtMC4wMDEsLTAuMDAxbC0yLjUzNSwtMzEuMDc5bDAsLTAuMDA3YzAuMDA0LDAgMC4wMDMsLTAuMDIzIC0wLjAwMSwtMC4wMjNjMC4wMDksLTAuMDAyIC0wLjAwNCwtMC4wNiAwLjA1NSwtMC4xMTdjMC4wNTIsLTAuMDU2IDAuMTExLC0wLjA0MyAwLjExOSwtMC4wNTJsMC4wMDEsMGMwLDAgMTYuMDAxLDAgNDguMDAzLDBaIiBzdHlsZT0iZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjwvZz48L2c+PGc+PHBhdGggZD0iTTQwLjc4LDcuODY4Yy0wLjkxLDAgLTEuNzg5LDAuMzMxIC0yLjQ3NCwwLjkzbC0wLjk5MywwLjg2OWMtMC40MzUsMC4zODEgLTAuOTk1LDAuNTkxIC0xLjU3NCwwLjU5MWwtMy45NTUsMGwwLDBsLTMuOTU0LDBjLTAuNTc5LDAgLTEuMTM5LC0wLjIxIC0xLjU3NSwtMC41OTFsLTAuOTkzLC0wLjg2OWMtMC42ODQsLTAuNTk5IC0xLjU2MywtMC45MyAtMi40NzMsLTAuOTNsLTcuNDAyLDBsMCwyLjY0Nmw4LjQ0NywwYzAuNzM4LDAgMS40NSwwLjI2OSAyLjAwMywwLjc1NmwwLjYwOCwwLjUzNWMwLjQ3NSwwLjQxOCAxLjA4NywwLjY0OSAxLjcyLDAuNjQ5bDcuMjM4LDBjMC42MzQsMCAxLjI0NSwtMC4yMzEgMS43MjEsLTAuNjQ5bDAuNjA3LC0wLjUzNWMwLjU1NCwtMC40ODcgMS4yNjYsLTAuNzU2IDIuMDA0LC0wLjc1Nmw4Ljg3NSwwbDAsLTIuNjQ2bC03LjgzLDBabS01LjA2LDI4LjU2N2wtNy44NzIsMGMtMC41MzcsMCAtMS4wNSwwLjIyMiAtMS40MTcsMC42MTRsLTAuNzg5LDAuODQzYy0wLjM5NCwwLjQyIC0wLjk0NCwwLjY1OSAtMS41MjEsMC42NTlsLTguNzM0LDBsMCwyLjE0OGw3LjkwMSwwYzAuNjg5LDAgMS4zNDQsLTAuMjk1IDEuODAxLC0wLjgxMWwwLjc3LC0wLjg2OGMwLjQ2NSwtMC41MjUgMS4xMzEsLTAuODI1IDEuODMyLC0wLjgyNWw4LjE4NywwYzAuNywwIDEuMzY3LDAuMyAxLjgzMSwwLjgyNGwwLjc3LDAuODY5YzAuNDU3LDAuNTE2IDEuMTEzLDAuODExIDEuODAyLDAuODExbDguMzI5LDBsMCwtMi4xNDhsLTkuMTYzLDBjLTAuNTc2LDAgLTEuMTI3LC0wLjIzOSAtMS41MjEsLTAuNjZsLTAuNzg4LC0wLjg0MmMtMC4zNjcsLTAuMzkyIC0wLjg4LC0wLjYxNCAtMS40MTgsLTAuNjE0WiIgc3R5bGU9ImZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNNDYuOTIxLDcuODY4bDAsMi42NDZsMy43MzcsMGMwLjgzMSwwIDEuNDg2LDAuNzA2IDEuNDIzLDEuNTM1bC0xLjkxMSwyNS4zMThjLTAuMDUsMC42NjggLTAuNjA2LDEuMTg0IC0xLjI3NiwxLjE4NGwtMS45NzMsMGwwLDIuMTQ4bDUuNTIyLDBjMC45MzksMCAxLjcyMiwtMC43MiAxLjgsLTEuNjU2bDIuNDMsLTI5LjIxN2MwLjA4OCwtMS4wNTQgLTAuNzQ0LC0xLjk1OCAtMS44MDEsLTEuOTU4bC03Ljk1MSwwWiIgc3R5bGU9ImZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNOC42OTYsNy44NjhjLTEuMDU3LDAgLTEuODg4LDAuOTA0IC0xLjgwMSwxLjk1OGwyLjQzMSwyOS4yMTdjMC4wNzgsMC45MzYgMC44NiwxLjY1NiAxLjc5OSwxLjY1Nmw1LjUxMSwwbDAsLTEuMjMxYy0wLjAwNSwtMC4zMDYgLTAuMDEsLTAuNjExIC0wLjAxNCwtMC45MTdsLTEuOTQ3LDBjLTAuNjcsMCAtMS4yMjYsLTAuNTE2IC0xLjI3NywtMS4xODRsLTEuOTExLC0yNS4zMThjLTAuMDYyLC0wLjgyOSAwLjU5MywtMS41MzUgMS40MjMsLTEuNTM1bDMuNzEsMGwwLC0yLjY0NmwtNy45MjQsMFoiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PGc+PGc+PHBhdGggZD0iTTUxLjI1OSwxMC4xNjRsMCwtMC40MjlsLTExLjQxNywwbC0wLjAwNywwYzAsMCAtMC4xNTIsLTAuMDAzIC0wLjM3NiwwLjAyM2MtMC4yMjYsMC4wMjQgLTAuNTIzLDAuMDkgLTAuODA0LDAuMTk4Yy0wLjI4MiwwLjEwOSAtMC41NDUsMC4yNjEgLTAuNzI4LDAuMzk0Yy0wLjE4NCwwLjEzMSAtMC4yOTQsMC4yMzQgLTAuMjk0LDAuMjM0bC0wLjAwNSwwLjAwNWwtMC42MjIsMC41NTlsLTAuMDA1LDAuMDA0YzAsMCAtMC4zMzIsMC4zMDkgLTAuNzU1LDAuNDYzYy0wLjQxNywwLjE3IC0wLjg3LDAuMTY0IC0wLjg3LDAuMTY0bC0wLjAwNiwwbC0zLjgsMGwwLDAuMjE0bDAuMjE0LC0wLjIxNGwtMy42NDMsMGMwLDAgLTAuNDUyLDAuMDA3IC0wLjg2OSwtMC4xNjNjLTAuNDIzLC0wLjE1NSAtMC43NTYsLTAuNDYzIC0wLjc1NiwtMC40NjNsLTAuMDA0LC0wLjAwNGwtMC42MjIsLTAuNTU5bC0wLjAwNSwtMC4wMDVjMCwwIC0wLjExLC0wLjEwMyAtMC4yOTQsLTAuMjM0Yy0wLjE4MywtMC4xMzMgLTAuNDQ2LC0wLjI4NSAtMC43MjgsLTAuMzk0Yy0wLjI4MiwtMC4xMDkgLTAuNTc4LC0wLjE3NSAtMC44MDMsLTAuMTk5Yy0wLjIyNSwtMC4wMjYgLTAuMzc3LC0wLjAyMyAtMC4zNzcsLTAuMDIzbC0wLjAwNywwbC0xMS4zNjIsMGwtMC4wMDcsMGMwLDAgLTAuMDQ0LDAuMDAyIC0wLjEyLDAuMDA2Yy0wLjA3NiwwLjAwNCAtMC4xODYsMC4wMTggLTAuMzEzLDAuMDUzYy0wLjI1NSwwLjA2MyAtMC41NzQsMC4yMzYgLTAuODA3LDAuNDk3Yy0wLjIzNiwwLjI2IC0wLjM3NSwwLjU5MiAtMC40MTUsMC44NWMtMC4wNDYsMC4yNTggLTAuMDI1LDAuNDMyIC0wLjAyNSwwLjQzMmwwLDAuMDA4bDEuOTcxLDI2LjY4bDAuMDAxLDAuMDA4YzAsMCAwLjAwNSwwLjE0NCAwLjA2OSwwLjM1MWMwLjA2LDAuMjA3IDAuMjAyLDAuNDY2IDAuNDEyLDAuNjY2YzAuMjA5LDAuMiAwLjQ3NiwwLjMzIDAuNjg5LDAuMzc3YzAuMjExLDAuMDUyIDAuMzU4LDAuMDQ1IDAuMzU4LDAuMDQ1bDkuNzU3LDBjMCwwIDAuMTIyLDAuMDAzIDAuMzAyLC0wLjAyMWMwLjE4LC0wLjAyMiAwLjQxNywtMC4wODIgMC42MzgsLTAuMThjMC4yMjEsLTAuMDk4IDAuNDIzLC0wLjIzNCAwLjU2LC0wLjM1MmMwLjEzNywtMC4xMTYgMC4yMjIsLTAuMjEzIDAuMjIyLC0wLjIxM2wwLjAwNSwtMC4wMDVsMC45MzUsLTEuMTQ0bDAuMDA1LC0wLjAwNWMwLDAgMC4wNTQsLTAuMDYzIDAuMTYxLC0wLjE1M2MwLjEwNCwtMC4wOSAwLjI1NywtMC4xOTIgMC40MjQsLTAuMjY2YzAuMTY3LC0wLjA3NCAwLjM0NSwtMC4xMTkgMC40ODEsLTAuMTM2YzAuMTM2LC0wLjAxOCAwLjIyNiwtMC4wMTYgMC4yMjYsLTAuMDE2bDAuMDA3LDBsNy44NjYsMC4wNTJsMC4wMDcsMGMwLDAgMC4zNzMsLTAuMDA2IDAuNzA5LDAuMTUxYzAuMTY2LDAuMDc0IDAuMzIsMC4xNzYgMC40MjQsMC4yNjZjMC4xMDUsMC4wODkgMC4xNjMsMC4xNTUgMC4xNjMsMC4xNTVsMC4wMDQsMC4wMDRsMC45MzUsMS4wOTJsMC4wMDUsMC4wMDZjMCwwIDAuMzI4LDAuMzczIDAuNzgsMC41NjNjMC4yMjEsMC4wOTkgMC40NTgsMC4xNTkgMC42MzgsMC4xODFjMC4xOCwwLjAyNCAwLjMwMiwwLjAyMSAwLjMwMiwwLjAyMWwwLjAwNywwbDkuNzUsMGMwLDAgMC4xNDcsMC4wMDcgMC4zNTgsLTAuMDQ0YzAuMjEyLC0wLjA0OCAwLjQ4MSwtMC4xNzcgMC42OSwtMC4zNzdjMC4yMSwtMC4xOTkgMC4zNTIsLTAuNDU5IDAuNDEyLC0wLjY2NmMwLjA2NCwtMC4yMDYgMC4wNjksLTAuMzUxIDAuMDY5LC0wLjM1MWwwLjAwMSwtMC4wMDhsMS45NzIsLTI2LjY3OWwwLC0wLjAwOGMwLDAgMC4wMDEsLTAuMDU4IDAuMDA0LC0wLjE3NGMtMC4wMDksLTAuMTE1IC0wLjAxNiwtMC4yOTIgLTAuMDkzLC0wLjUxMmMtMC4wNzMsLTAuMjIgLTAuMjE4LC0wLjQ4MyAtMC40NzQsLTAuNzIyYy0wLjI1OCwtMC4yMyAtMC42NDcsLTAuNDMyIC0xLjEyLC0wLjQzMmwwLDAuNDI5YzAuMzUyLDAuMDAxIDAuNjM3LDAuMTUgMC44MzEsMC4zMjJjMC4xOTIsMC4xNzkgMC4zMDIsMC4zNzYgMC4zNTYsMC41NDJjMC4wNTksMC4xNjUgMC4wNjMsMC4yOTkgMC4wNzEsMC4zODZjLTAuMDAyLDAuMDg3IC0wLjAwNCwwLjEzMSAtMC4wMDQsMC4xMzFsMCwwLjAwNmwtMS45NzEsMjYuNjhsLTAuMDAxLDAuMDA1YzAsMCAtMC4wMDQsMC4xMDcgLTAuMDUxLDAuMjU3Yy0wLjA0MywwLjE1IC0wLjE0NywwLjMzOCAtMC4yOTksMC40ODNjLTAuMTUxLDAuMTQ0IC0wLjM0MywwLjIzNyAtMC40OTUsMC4yNzFjLTAuMTUxLDAuMDM2IC0wLjI1NiwwLjAzMiAtMC4yNTYsMC4wMzJsLTkuNzQ5LDBsLTAuMDA3LDBjMCwwIC0wLjQwMywwLjAwNiAtMC43NjYsLTAuMTY1Yy0wLjE4LC0wLjA4IC0wLjM0NiwtMC4xOTIgLTAuNDU4LC0wLjI4OWMtMC4xMTQsLTAuMDk3IC0wLjE3NiwtMC4xNjggLTAuMTc2LC0wLjE2OGwtMC4wMDUsLTAuMDA0bC0wLjkzNCwtMS4wOTNsLTAuMDA2LC0wLjAwNWMwLDAgLTAuMzA5LC0wLjM0OSAtMC43MzMsLTAuNTI4Yy0wLjIwOCwtMC4wOTIgLTAuNDMsLTAuMTQ4IC0wLjU5OSwtMC4xNjhjLTAuMTY5LC0wLjAyMiAtMC4yODIsLTAuMDIgLTAuMjgyLC0wLjAybC0wLjAwNywwbC03Ljg2NywtMC4wNTFsLTAuMDA3LDBjMCwwIC0wLjExNSwtMC4wMDMgLTAuMjg0LDAuMDE5Yy0wLjE2OSwwLjAyMSAtMC4zOTEsMC4wNzcgLTAuNTk4LDAuMTY5Yy0wLjIwOCwwLjA5MiAtMC4zOTgsMC4yMiAtMC41MjcsMC4zMzFjLTAuMTI4LDAuMTA4IC0wLjIwOCwwLjE5OSAtMC4yMDgsMC4xOTlsLTAuMDA1LDAuMDA2bC0wLjkzNSwxLjE0M2wtMC4wMDUsMC4wMDVjMCwwIC0wLjA1OSwwLjA2OCAtMC4xNzQsMC4xNjZjLTAuMTEzLDAuMDk3IC0wLjI3OCwwLjIwOCAtMC40NTksMC4yODljLTAuMzYyLDAuMTcgLTAuNzY2LDAuMTY0IC0wLjc2NiwwLjE2NGwtOS43NTUsMGMwLDAgLTAuMTA2LDAuMDA0IC0wLjI1NywtMC4wMzNjLTAuMTUxLC0wLjAzMyAtMC4zNDMsLTAuMTI2IC0wLjQ5NSwtMC4yNzFjLTAuMTUyLC0wLjE0NSAtMC4yNTUsLTAuMzMzIC0wLjI5OCwtMC40ODRjLTAuMDQ2LC0wLjE1IC0wLjA1MSwtMC4yNTYgLTAuMDUxLC0wLjI1NmwwLC0wLjAwNmwtMS45NzIsLTI2LjY3OWwwLC0wLjAwNmMwLDAgLTAuMDE1LC0wLjEzMyAwLjAxOSwtMC4zMjZjMC4wMzEsLTAuMTk1IDAuMTM1LC0wLjQ0NCAwLjMxMSwtMC42MzljMC4xNzYsLTAuMTk2IDAuNDEzLC0wLjMyNCAwLjYwMiwtMC4zN2MwLjE4OCwtMC4wNSAwLjMyLC0wLjA0NSAwLjMyLC0wLjA0NWwwLjAwNywwbDExLjM2MiwwbDAuMDA2LDBjMCwwIDAuMTMyLC0wLjAwMiAwLjMyNywwLjAyMWMwLjE5NiwwLjAyMSAwLjQ1NCwwLjA3OCAwLjY5OSwwLjE3M2MwLjQ5OSwwLjE4MiAwLjg5LDAuNTQ3IDAuODksMC41NDdsMC4wMDUsMC4wMDRsMC42MjIsMC41NTlsMC4wMDUsMC4wMDVjMCwwIDAuMzksMC4zNjIgMC44ODcsMC41NDRjMC4yNDUsMC4wOTQgMC41MDIsMC4xNSAwLjY5NywwLjE3MWMwLjE5NSwwLjAyMyAwLjMyNywwLjAyIDAuMzI3LDAuMDJsMC4wMDcsMGwzLjg1MSwwbDAsLTAuMjE1Yy0wLjA3MiwwLjA3MiAtMC4xNDMsMC4xNDMgLTAuMjE1LDAuMjE1bDMuNTkzLDBjMCwwIDAuMTMxLDAuMDAyIDAuMzI3LC0wLjAyYzAuMTk1LC0wLjAyMSAwLjQ1MiwtMC4wNzggMC42OTcsLTAuMTcyYzAuNDk3LC0wLjE4MiAwLjg4NywtMC41NDQgMC44ODcsLTAuNTQ0bDAuMDA1LC0wLjAwNWwwLjYyMiwtMC41NTlsMC4wMDUsLTAuMDA0YzAsMCAwLjM5MSwtMC4zNjUgMC44OSwtMC41NDdjMC4yNDUsLTAuMDk1IDAuNTAzLC0wLjE1MiAwLjY5OSwtMC4xNzNjMC4xOTUsLTAuMDIyIDAuMzI2LC0wLjAyIDAuMzI2LC0wLjAybDExLjQyNCwwWiIgc3R5bGU9ImZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxwYXRoIGQ9Ik0yNy44NDMsMzUuNzY0bDcuODgyLDBjMC43MDksMCAxLjQxOCwwLjMxNSAxLjg5MSwwLjgyN2wwLjc4OCwwLjgyOGMwLjI3NiwwLjI3NiAwLjY3LDAuNDczIDEuMDI1LDAuNDczbDkuNDU4LDBjMC4zMTUsMCAwLjU5MSwtMC4yNzYgMC41OTEsLTAuNTkybDEuOTMxLC0yNS4yOTljMCwtMC4xOTcgLTAuMDM5LC0wLjQzNCAtMC4xOTcsLTAuNTUyYy0wLjE1OCwtMC4xNTcgLTAuMzU1LC0wLjI3NiAtMC41NTIsLTAuMjc2bC0xMC45MTYsMGMtMC41OTEsMCAtMS4xNDIsMC4yMzcgLTEuNTc2LDAuNTkybC0wLjU5MSwwLjU1MWMtMC41OTEsMC41MTMgLTEuMzc5LDAuODI4IC0yLjE2NywwLjgyOGwtNy4yNTEsMGMtMC43ODksMCAtMS41NzcsLTAuMzE1IC0yLjE2OCwtMC44MjhsLTAuNTkxLC0wLjU1MWMtMC40MzMsLTAuMzU1IC0wLjk4NSwtMC41OTIgLTEuNTc2LC0wLjU5MmwtMTAuOTE2LDBjLTAuMTk3LDAgLTAuMzk0LDAuMTE5IC0wLjU1MiwwLjI3NmMtMC4xNTcsMC4xMTggLTAuMTk3LDAuMzU1IC0wLjE5NywwLjU1MmwxLjkzMSwyNS4yOTljMCwwLjMxNiAwLjI3NiwwLjU5MiAwLjU5MSwwLjU5Mmw5LjQ1OCwwYzAuMzU1LDAgMC43NDksLTAuMTk3IDEuMDI1LC0wLjQ3M2wwLjc4OCwtMC44MjhjMC40NzMsLTAuNTEyIDEuMTgyLC0wLjgyNyAxLjg5MSwtMC44MjdabTIxLjA0NCwyLjIwNmwtOS40NTgsMGMtMC4zOTQsMCAtMC43ODgsLTAuMTk3IC0xLjA2NCwtMC40NzNsLTAuNzg4LC0wLjg2NmMtMC40NzMsLTAuNTEzIC0xLjE0MywtMC43ODkgLTEuODUyLC0wLjc4OWwtNy44ODIsMGMtMC43MDksMCAtMS4zNzksMC4yNzYgLTEuODUyLDAuNzg5bC0wLjc4OCwwLjg2NmMtMC4yNzYsMC4yNzYgLTAuNjcsMC40NzMgLTEuMDY0LDAuNDczbC05LjQ1OCwwYy0wLjM1NCwwIC0wLjY3LC0wLjI3NSAtMC42NywtMC42M2wtMS45MzEsLTI1LjMzOWMwLC0wLjIzNiAwLjA3OSwtMC40MzQgMC4yMzcsLTAuNjMxYzAuMTU3LC0wLjE1NyAwLjM1NSwtMC4yNzUgMC41OTEsLTAuMjc1bDEwLjkxNiwwYzAuNTkxLDAgMS4xODIsMC4yMzYgMS42MTUsMC42M2wwLjU5MiwwLjUxMmMwLjU5MSwwLjUxMyAxLjMzOSwwLjgyOCAyLjEyOCwwLjgyOGw3LjI1MSwwYzAuNzg4LDAgMS41MzYsLTAuMzE1IDIuMTI4LC0wLjgyOGwwLjU5MSwtMC41MTJjMC40MzMsLTAuMzk0IDEuMDI0LC0wLjYzIDEuNjE1LC0wLjYzbDEwLjkxNiwwYzAuMjM3LDAgMC40MzQsMC4xMTggMC41OTEsMC4yNzVjMC4xNTgsMC4xOTcgMC4yMzcsMC4zOTUgMC4yMzcsMC42MzFsLTEuOTMxLDI1LjMzOWMwLDAuMzU1IC0wLjMxNSwwLjYzIC0wLjY3LDAuNjNaIiBzdHlsZT0iZmlsbDojZmZmO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48L2c+PC9nPjxnPjxnPjxnPjxwYXRoIGQ9Ik0yNy44NTEsMzUuNDg4bDcuODY2LDBjMC44NDgsMCAxLjY1OCwwLjM0NyAyLjIzNywwLjk2NGwwLjc3MSwwLjg4N2MwLjE5MiwwLjE5MyAwLjQ2MiwwLjMwOCAwLjczMiwwLjMwOGw5LjQ0NywwYzAuMDc3LDAgMC4xNTQsLTAuMDc3IDAuMTU0LC0wLjE5MmwxLjkyOCwtMjUuNjhjMCwtMC4wNzcgLTAuMDM4LC0wLjE1NCAtMC4wNzcsLTAuMjMyYy0wLjA3NywtMC4wNzcgLTAuMTU0LC0wLjExNSAtMC4yNywtMC4xMTVsLTEwLjkxMiwwYy0wLjQ2MywwIC0wLjkyNSwwLjE5MyAtMS4yNzIsMC41MDFsLTAuNjE3LDAuNTRjLTAuNjU2LDAuNjE3IC0xLjU0MywwLjkyNSAtMi40MjksMC45MjVsLTcuMjQ5LDBjLTAuODg3LDAgLTEuNzc0LC0wLjMwOCAtMi40MjksLTAuOTI1bC0wLjYxNywtMC41NGMtMC4zNDcsLTAuMzA4IC0wLjgxLC0wLjUwMSAtMS4yNzMsLTAuNTAxbC0xMC45MTIsMGMtMC4xMTUsMCAtMC4xOTMsMC4wMzggLTAuMjcsMC4xMTVjLTAuMDM4LDAuMDc4IC0wLjA3NywwLjE1NSAtMC4wNzcsMC4yMzJsMS45MjgsMjUuNjhjMCwwLjExNSAwLjA3NywwLjE5MiAwLjE1NCwwLjE5Mmw5LjQ0NywwYzAuMjcsMCAwLjU0LC0wLjExNSAwLjczMywtMC4zMDhsMC43NzEsLTAuODg3YzAuNTc4LC0wLjYxNyAxLjM4OCwtMC45NjQgMi4yMzYsLTAuOTY0Wm0yMS4wNTMsMi4yMzZsLTkuNDQ3LDBjLTAuMzA4LDAgLTAuNTc4LC0wLjExNSAtMC43NzEsLTAuMzQ3bC0wLjgxLC0wLjg0OGMtMC41MzksLTAuNjE3IC0xLjM0OSwtMC45NjQgLTIuMTU5LC0wLjk2NGwtNy44NjYsMGMtMC44MDksMCAtMS42MTksMC4zNDcgLTIuMTU5LDAuOTY0bC0wLjgxLDAuODQ4Yy0wLjE5MywwLjIzMiAtMC40NjIsMC4zNDcgLTAuNzcxLDAuMzQ3bC05LjQ0NywwYy0wLjExNSwwIC0wLjIzMSwtMC4xMTUgLTAuMjMxLC0wLjIzMWwtMS45MjgsLTI1LjY4YzAsLTAuMTE1IDAuMDM5LC0wLjIzMSAwLjExNiwtMC4zMDhjMC4wNzcsLTAuMTE2IDAuMTU0LC0wLjE1NCAwLjMwOCwtMC4xNTRsMTAuOTEyLDBjMC41MDEsMCAwLjk2NCwwLjE5MiAxLjMxMSwwLjUwMWwwLjYxNywwLjU0YzAuNjU2LDAuNjE3IDEuNTA0LDAuOTI1IDIuMzkxLDAuOTI1bDcuMjQ5LDBjMC44ODYsMCAxLjczNSwtMC4zMDggMi4zOSwtMC45MjVsMC42MTcsLTAuNTRjMC4zNDcsLTAuMzA5IDAuODQ4LC0wLjUwMSAxLjMxMSwtMC41MDFsMTAuOTEyLDBjMC4xMTYsMCAwLjIzMiwwLjAzOCAwLjMwOSwwLjE1NGMwLjA3NywwLjA3NyAwLjExNSwwLjE5MyAwLjExNSwwLjMwOGwtMS45MjgsMjUuNjhjMCwwLjExNiAtMC4xMTUsMC4yMzEgLTAuMjMxLDAuMjMxWiIgc3R5bGU9ImZpbGw6I2VhZmFmZjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PC9nPjwvZz48Zz48cGF0aCBkPSJNNy41OTQsMzQuNzc5bC0wLjAyMywwLjA4N2wtMC4wMjIsMC4wODdjMCwwIC0wLjAwNywwLjAzIC0wLjAyMywwLjA4N2MwLjU1MiwwLjEzMyAxLjA2MywwLjEyOSAxLjUyMywwLjE1NGMwLjQ2MSwwLjAyIDAuODc4LDAuMDYyIDEuMjQ5LDAuMTU1YzAuNzQ4LDAuMTczIDEuMjcyLDAuNTY2IDEuNjQ0LDAuOTFjMC4wOSwwLjA5MSAwLjE3NCwwLjE3NiAwLjI1MywwLjI1NWMwLjA3NiwwLjA4NCAwLjEzOSwwLjE2OCAwLjIwMSwwLjI0MmMwLjA2MSwwLjA3NSAwLjExNywwLjE0NCAwLjE2OCwwLjIwNmMwLjA0NSwwLjA2NyAwLjA4NiwwLjEyOCAwLjEyMiwwLjE4MWMwLjE0NiwwLjIxNSAwLjIxOCwwLjMyMyAwLjIxOCwwLjMyM2wwLjAwNCwwLjAwNmMwLDAgMC4xMjgsMC4yMTggMC4zMiwwLjU0NmMwLjE3NiwwLjMzNCAwLjQxNCwwLjc4MiAwLjU4MywxLjI1NGMwLjE3OCwwLjQ2OSAwLjI5OCwwLjk1NSAwLjMzOSwxLjMyMmMwLjA0NSwwLjM2NyAwLjAyNCwwLjYwOCAwLjAyNCwwLjYwOGMwLDAgLTAuMDA0LDAuMDQ5IC0wLjAxNCwwLjE0NmMtMC4wMTEsMC4wODkgLTAuMDMzLDAuMjU2IC0wLjA5MSwwLjM5N2MwLjA1NCwwLjAyNyAwLjA4MSwwLjA0IDAuMDgxLDAuMDRsMC4wODEsMC4wNGwwLjA4MSwwLjAzOWMwLDAgMC4wMjcsMC4wMTQgMC4wODEsMC4wNGMwLjEwMSwtMC4yNDYgMC4xMDcsLTAuMzk4IDAuMTI2LC0wLjUxNGMwLjAxLC0wLjExMSAwLjAxNSwtMC4xNjYgMC4wMTUsLTAuMTY2YzAsMCAwLjAyMywtMC4yNzQgLTAuMDI2LC0wLjY3MmMtMC4wNDUsLTAuMzk5IC0wLjE3MywtMC45MTUgLTAuMzU5LC0xLjQwNmMtMC4xNzgsLTAuNDk1IC0wLjQyMywtMC45NTcgLTAuNjA1LC0xLjMwM2MtMC4xOTgsLTAuMzM3IC0wLjMyOSwtMC41NjEgLTAuMzI5LC0wLjU2MWwtMC4wMDQsLTAuMDA2YzAsMCAtMC4wNzYsLTAuMTEzIC0wLjIyOSwtMC4zMzljLTAuMDM4LC0wLjA1NiAtMC4wODEsLTAuMTE5IC0wLjEyOSwtMC4xOWMtMC4wNTMsLTAuMDY2IC0wLjExMywtMC4xMzkgLTAuMTc4LC0wLjIxOWMtMC4wNjUsLTAuMDc5IC0wLjEzMiwtMC4xNjggLTAuMjEzLC0wLjI1N2MtMC4wODUsLTAuMDg1IC0wLjE3NiwtMC4xNzcgLTAuMjc0LC0wLjI3NGMtMC4zOTgsLTAuMzcyIC0wLjk4NSwtMC44MDggLTEuODA0LC0wLjk5OGMtMC40MDQsLTAuMTAxIC0wLjg1LC0wLjE0NSAtMS4zMTgsLTAuMTY1Yy0wLjQ2NywtMC4wMjQgLTAuOTYzLC0wLjAyNSAtMS40NDksLTAuMTQyYy0wLjAxNSwwLjA1OCAtMC4wMjMsMC4wODcgLTAuMDIzLDAuMDg3WiIgc3R5bGU9ImZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48Zz48Zz48cGF0aCBkPSJNNTQuNTA2LDguMzY0bDAsLTAuODI5bC0xMy44NjcsMGwtMC4wMDgsMGMwLDAgLTAuMTg1LC0wLjAwNCAtMC40NTksMC4wMjdjLTAuMjc1LDAuMDI5IC0wLjYzNywwLjEwNiAtMC45ODMsMC4yMzZjLTAuMzQ1LDAuMTMgLTAuNjY5LDAuMzExIC0wLjg5NCwwLjQ3Yy0wLjIyNywwLjE1OCAtMC4zNjMsMC4yODMgLTAuMzYzLDAuMjgzbC0wLjAwNiwwLjAwNGwtMC45NzIsMC44NTFsLTAuMDA0LDAuMDA0YzAsMCAtMC4yNjIsMC4yMzYgLTAuNTkzLDAuMzUzYy0wLjMyNSwwLjEyOSAtMC42NzgsMC4xMjMgLTAuNjc4LDAuMTIzbC0wLjAwNiwwLjAwMWwtNC4zMDMsMGwwLDAuNDE0YzAuMTM4LC0wLjEzOCAwLjI3NiwtMC4yNzYgMC40MTQsLTAuNDE0bC0zLjg5NSwwYzAsMCAtMC4zNTMsMC4wMDUgLTAuNjc4LC0wLjEyNGMtMC4zMzEsLTAuMTE4IC0wLjU5MiwtMC4zNTQgLTAuNTkyLC0wLjM1NGwtMC4wMDUsLTAuMDA0bC0wLjk3MiwtMC44NWwtMC4wMDYsLTAuMDA1YzAsMCAtMC4xMzYsLTAuMTI0IC0wLjM2MywtMC4yODJjLTAuMjI2LC0wLjE2IC0wLjU1LC0wLjM0IC0wLjg5NSwtMC40N2MtMC4zNDUsLTAuMTMgLTAuNzA3LC0wLjIwNyAtMC45ODIsLTAuMjM2Yy0wLjI3NSwtMC4wMzEgLTAuNDU5LC0wLjAyNyAtMC40NTksLTAuMDI3bC0xMy44NzIsMGwtMC4wMDgsMGMwLDAgLTAuMDU2LDAuMDAzIC0wLjE1NSwwLjAwOGMtMC4wOTgsMC4wMDQgLTAuMjM5LDAuMDIzIC0wLjQwMywwLjA2N2MtMC4xNjUsMC4wMzggLTAuMzQ4LDAuMTE4IC0wLjUzNCwwLjIxOWMtMC4xODIsMC4xMDkgLTAuMzYxLDAuMjQ5IC0wLjUxNiwwLjQxM2MtMC4xNTEsMC4xNjggLTAuMjc2LDAuMzU3IC0wLjM2OSwwLjU0OGMtMC4wODUsMC4xOTMgLTAuMTUsMC4zODIgLTAuMTc1LDAuNTVjLTAuMDMsMC4xNjcgLTAuMDM3LDAuMzEgLTAuMDM0LDAuNDA4YzAuMDA0LDAuMDk4IDAuMDA1LDAuMTU0IDAuMDA1LDAuMTU0bDAuMDAxLDAuMDA4bDIuMzkyLDI4Ljc1MmwwLDAuMDA4YzAsMCAwLjAwNywwLjA1IDAuMDE4LDAuMTM4YzAuMDEyLDAuMDg4IDAuMDM3LDAuMjEzIDAuMDg1LDAuMzU3YzAuMDg5LDAuMjkyIDAuMjk2LDAuNjU1IDAuNTk3LDAuOTNjMC4yOTksMC4yNzcgMC42NzgsMC40NTMgMC45NzYsMC41MTdjMC4xNDgsMC4wMzcgMC4yNzUsMC4wNTEgMC4zNjMsMC4wNTVjMC4wODksMC4wMDQgMC4xMzksMC4wMDcgMC4xMzksMC4wMDdsMC4wMDgsMGwxMS45NzIsMGMwLDAgMC4xNDcsMC4wMDMgMC4zNjUsLTAuMDI2YzAuMjE5LC0wLjAyNyAwLjUwNSwtMC4wOTkgMC43NzMsLTAuMjJjMC4yNjgsLTAuMTIgMC41MTIsLTAuMjg3IDAuNjc3LC0wLjQzMmMwLjE2NiwtMC4xNDUgMC4yNjEsLTAuMjU3IDAuMjYxLC0wLjI1N2wwLjAwNSwtMC4wMDVsMC43NTMsLTAuODVsMC4wMDQsLTAuMDA1YzAsMCAwLjA2OSwtMC4wOCAwLjE4OCwtMC4xODRjMC4xMTcsLTAuMTA0IDAuMjkzLC0wLjIyMyAwLjQ4NSwtMC4zMWMwLjE5MiwtMC4wODYgMC4zOTcsLTAuMTM4IDAuNTUzLC0wLjE1N2MwLjE1NiwtMC4wMiAwLjI2MiwtMC4wMTggMC4yNjIsLTAuMDE4bDQuNDQ2LDBsMCwtMC40MTVjLTAuMTM5LDAuMTM5IC0wLjI3NywwLjI3NyAtMC40MTUsMC40MTVsNC4wMjYsMGwwLjAwNiwwYzAsMCAwLjEwNiwtMC4wMDIgMC4yNjIsMC4wMThjMC4xNTYsMC4wMTkgMC4zNjIsMC4wNzEgMC41NTQsMC4xNThjMC4xOTEsMC4wODYgMC4zNjcsMC4yMDUgMC40ODUsMC4zMWMwLjExOCwwLjEwMyAwLjE4NywwLjE4NCAwLjE4NywwLjE4NGwwLjAwNCwwLjAwNWwwLjc1MywwLjg1bDAuMDA1LDAuMDA1YzAsMCAwLjA5NSwwLjExMiAwLjI2MiwwLjI1NmMwLjE2NCwwLjE0NiAwLjQwOSwwLjMxMyAwLjY3NiwwLjQzM2MwLjI2OCwwLjEyIDAuNTU1LDAuMTkzIDAuNzczLDAuMjE5YzAuMjE4LDAuMDI5IDAuMzY2LDAuMDI2IDAuMzY2LDAuMDI2bDAuMDA3LDBsMTEuOTY1LDBsMC4wMDgsMGMwLDAgMC4wNSwtMC4wMDMgMC4xMzksLTAuMDA3YzAuMDg4LC0wLjAwNCAwLjIxNSwtMC4wMTkgMC4zNjMsLTAuMDU1YzAuMjk4LC0wLjA2NSAwLjY3NiwtMC4yNDEgMC45NzYsLTAuNTE4YzAuMywtMC4yNzUgMC41MDcsLTAuNjM4IDAuNTk2LC0wLjkzYzAuMDQ4LC0wLjE0NCAwLjA3MywtMC4yNyAwLjA4NCwtMC4zNTdjMC4wMTIsLTAuMDg4IDAuMDE4LC0wLjEzOCAwLjAxOCwtMC4xMzhsMC4wMDEsLTAuMDA4bDIuMzkyLC0yOC43NTJsMCwtMC4wMDhjMCwwIDAuMDAzLC0wLjA3NSAwLjAwNywtMC4yMjVjLTAuMDExLC0wLjE1IC0wLjAxOSwtMC4zOCAtMC4xMiwtMC42NjZjLTAuMDk3LC0wLjI4NSAtMC4yODgsLTAuNjI0IC0wLjYyNCwtMC45MjhjLTAuMzM2LC0wLjI5NSAtMC44NDIsLTAuNTQ3IC0xLjQ0OSwtMC41NDdsMCwwLjgyOWMwLjM4MSwwLjAwMSAwLjY4OSwwLjE1NyAwLjkwMSwwLjM0YzAuMjA4LDAuMTkxIDAuMzMsMC40MDEgMC4zODksMC41NzdjMC4wNjMsMC4xNzggMC4wNjUsMC4zMjEgMC4wNzQsMC40MTNjLTAuMDAzLDAuMDk0IC0wLjAwNCwwLjE0MiAtMC4wMDQsMC4xNDJsMCwwLjAwNWwtMi4zOTIsMjguNzUxbC0wLjAwMSwwLjAwNmMwLDAgLTAuMDA2LDAuMTI4IC0wLjA2NCwwLjMwOGMtMC4wNTQsMC4xODIgLTAuMTgzLDAuNDA2IC0wLjM3LDAuNTc4Yy0wLjE4NywwLjE3MiAtMC40MjEsMC4yODIgLTAuNjA2LDAuMzIyYy0wLjE4NSwwLjA0MyAtMC4zMTMsMC4wMzggLTAuMzEzLDAuMDM4bC0wLjAwNSwwLjAwMWwtMTEuOTcxLDBjMCwwIC0wLjQyMiwwLjAwNyAtMC43OTksLTAuMTcyYy0wLjM4NSwtMC4xNjQgLTAuNjU5LC0wLjQ4NCAtMC42NTksLTAuNDg0bC0wLjAwNSwtMC4wMDVsLTAuNzUzLC0wLjg0OWwtMC4wMDUsLTAuMDA2YzAsMCAtMC4wOTYsLTAuMTE0IC0wLjI2NCwtMC4yNmMtMC4xNjgsLTAuMTQ4IC0wLjQxNSwtMC4zMTcgLTAuNjg3LC0wLjQzOWMtMC4yNzEsLTAuMTIzIC0wLjU2MiwtMC4xOTYgLTAuNzg0LC0wLjIyM2MtMC4yMjEsLTAuMDI5IC0wLjM3LC0wLjAyNiAtMC4zNywtMC4wMjZsLTQuNDQ4LDBsMCwwLjQxNGMwLjEzOCwtMC4xMzggMC4yNzYsLTAuMjc2IDAuNDE0LC0wLjQxNGwtNC4wMjUsMGwtMC4wMDcsMGMwLDAgLTAuMTUsLTAuMDAzIC0wLjM3MSwwLjAyNmMtMC4yMjEsMC4wMjcgLTAuNTEyLDAuMSAtMC43ODQsMC4yMjNjLTAuMjcxLDAuMTIyIC0wLjUxOSwwLjI5MSAtMC42ODYsMC40MzhjLTAuMTY5LDAuMTQ3IC0wLjI2NSwwLjI2MSAtMC4yNjUsMC4yNjFsLTAuMDA1LDAuMDA1bC0wLjc1MywwLjg1bC0wLjAwNCwwLjAwNWMwLDAgLTAuMjc1LDAuMzIgLTAuNjU5LDAuNDg0Yy0wLjM3OCwwLjE3OSAtMC43OTksMC4xNzIgLTAuNzk5LDAuMTcybC0wLjAwNiwwbC0xMS45NjUsMGwtMC4wMDYsMGMwLDAgLTAuMTI4LDAuMDA0IC0wLjMxMywtMC4wMzljLTAuMTg0LC0wLjAzOSAtMC40MTksLTAuMTQ5IC0wLjYwNiwtMC4zMjFjLTAuMTg3LC0wLjE3MyAtMC4zMTYsLTAuMzk3IC0wLjM3LC0wLjU3OGMtMC4wNTgsLTAuMTggLTAuMDY1LC0wLjMwOCAtMC4wNjUsLTAuMzA4bDAsLTAuMDA2bC0yLjM5MiwtMjguNzUxbDAsLTAuMDA2YzAsMCAtMC4wMTgsLTAuMTQyIDAuMDE4LC0wLjM1YzAuMDMyLC0wLjIwNyAwLjE0NiwtMC40NzMgMC4zMzgsLTAuNjgyYzAuMTkyLC0wLjIwOSAwLjQ0OCwtMC4zNDQgMC42NTMsLTAuMzkyYzAuMTAxLC0wLjAyOCAwLjE4OSwtMC4wNDEgMC4yNSwtMC4wNDJjMC4wNjIsLTAuMDA0IDAuMDk3LC0wLjAwNiAwLjA5NywtMC4wMDZsMC4wMDYsMGwxMy44NywwYzAsMCAwLjE0OCwtMC4wMDMgMC4zNjcsMC4wMjJjMC4yMiwwLjAyMiAwLjUwOSwwLjA4NCAwLjc4NCwwLjE4OGMwLjI3NiwwLjEwMyAwLjUzNSwwLjI0OCAwLjcxNSwwLjM3NWMwLjE4MSwwLjEyNiAwLjI5LDAuMjI1IDAuMjksMC4yMjVsMC4wMDUsMC4wMDVsMC45NzIsMC44NWwwLjAwNiwwLjAwNWMwLDAgMC4zNzQsMC4zMzcgMC44NDUsMC41MDVjMC40NjUsMC4xODQgMC45NjgsMC4xNzYgMC45NjgsMC4xNzZsNC4zMTIsMGwwLC0wLjQxNGwtMC40MTUsMC40MTRsMy44OTYsMGMwLDAgMC41MDQsMC4wMDggMC45NjksLTAuMTc2YzAuNDcxLC0wLjE2NyAwLjg0NSwtMC41MDQgMC44NDUsLTAuNTA0bDAuMDA2LC0wLjAwNWwwLjk3MiwtMC44NTFsMC4wMDUsLTAuMDA0YzAsMCAwLjEwOSwtMC4xIDAuMjkxLC0wLjIyNmMwLjE3OSwtMC4xMjcgMC40MzgsLTAuMjcxIDAuNzEzLC0wLjM3NWMwLjI3NiwtMC4xMDQgMC41NjUsLTAuMTY2IDAuNzg1LC0wLjE4OGMwLjIxOSwtMC4wMjUgMC4zNjYsLTAuMDIyIDAuMzY2LC0wLjAyMmwxMy44NzQsMFoiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PC9nPjwvZz48Zz48cGF0aCBkPSJNNTYuNjczLDkuODI1bC0yLjQzLDI5LjIxOGMtMC4wNzgsMC45MzYgLTAuODYxLDEuNjU1IC0xLjgsMS42NTVsLTEyLjE2MiwwYy0wLjY4OSwwIC0xLjM0NSwtMC4yOTQgLTEuODAyLC0wLjgxbC0wLjc2OSwtMC44NjljLTAuNDY1LC0wLjUyNCAtMS4xMzIsLTAuODI0IC0xLjgzMiwtMC44MjRsLTguMTg3LDBjLTAuNzAxLDAgLTEuMzY3LDAuMyAtMS44MzIsMC44MjRsLTAuNzcsMC44NjljLTAuNDU2LDAuNTE2IC0xLjExMiwwLjgxIC0xLjgwMSwwLjgxbC0xMi4xNjMsMGMtMC45MzksMCAtMS43MjEsLTAuNzE5IC0xLjc5OSwtMS42NTVsLTIuNDMxLC0yOS4yMThjLTAuMDg3LC0xLjA1MyAwLjc0NCwtMS45NTcgMS44MDEsLTEuOTU3bDE0LjA5MywwYzAuOTEsMCAxLjc4OSwwLjMzMSAyLjQ3MywwLjkzbDAuOTkzLDAuODY5YzAuNDM2LDAuMzgxIDAuOTk2LDAuNTkxIDEuNTc1LDAuNTkxbDMuOTU0LDBsMCwwbDMuOTU1LDBjMC41NzksMCAxLjEzOSwtMC4yMSAxLjU3NCwtMC41OTFsMC45OTMsLTAuODY5YzAuNjg1LC0wLjU5OSAxLjU2NCwtMC45MyAyLjQ3NCwtMC45M2wxNC4wOTIsMGMxLjA1OCwwIDEuODg5LDAuOTA0IDEuODAxLDEuOTU3bS0wLjY1MiwtNC42ODhsLTQ4LjQ3NCwwLjAwMWMtMi4wMzYsMCAtMy42MzcsMS43NCAtMy40NjgsMy43NjhsMi41NjksMzAuODkyYzAuMTcsMi4wNTMgMS44ODcsMy42MzIgMy45NDYsMy42MzJsNDIuMzgsMGMyLjA2LDAgMy43NzYsLTEuNTggMy45NDcsLTMuNjMybDIuNTY5LC0zMC44OTJjMC4xNjksLTIuMDI5IC0xLjQzMywtMy43NjkgLTMuNDY5LC0zLjc2OSIgc3R5bGU9ImZpbGw6I2ZmZjtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PGc+PGc+PHBhdGggZD0iTTU1LjE4LDcuMDgzbDAsLTAuODQybC0xNS42LDBjMCwwIC0wLjQ2MiwtMC4wMDYgLTAuODg4LDAuMTYzYy0wLjQzMiwwLjE1NCAtMC43NzQsMC40NjMgLTAuNzc0LDAuNDYzbC0wLjAwNiwwLjAwNWwtMS41NjUsMS4zNjlsLTAuMDA0LDAuMDA0YzAsMCAtMC4yLDAuMTggLTAuNDUyLDAuMjdjLTAuMjQ5LDAuMDk4IC0wLjUxOCwwLjA5NSAtMC41MTgsMC4wOTVsLTAuMDA2LDBsLTQuMDA0LDBsMCwwLjQyMWMwLjE0MSwtMC4xNDEgMC4yODEsLTAuMjgxIDAuNDIxLC0wLjQyMWwtMy41ODgsMGMwLDAgLTAuMjY5LDAuMDA0IC0wLjUxOCwtMC4wOTVjLTAuMjUyLC0wLjA4OSAtMC40NTIsLTAuMjcgLTAuNDUyLC0wLjI3bC0wLjAwNSwtMC4wMDRsLTEuNTY1LC0xLjM2OWwtMC4wMDUsLTAuMDA1YzAsMCAtMC4zNDMsLTAuMzA5IC0wLjc3NSwtMC40NjNjLTAuNDI2LC0wLjE2OSAtMC44ODcsLTAuMTYzIC0wLjg4NywtMC4xNjNsLTAuMDA4LDBsLTE1LjU4OSwwbC0wLjAwNywwYzAsMCAtMC4wNzQsMC4wMDQgLTAuMjA1LDAuMDExYy0wLjEyOSwwLjAwMyAtMC4zMTUsMC4wMzEgLTAuNTMzLDAuMDgyYy0wLjQzMywwLjEwNSAtMC45OTYsMC4zNjEgLTEuNDE2LDAuNzljLTAuNDM3LDAuNDExIC0wLjcwOSwwLjk2NSAtMC44MjMsMS4zOTZjLTAuMDU3LDAuMjE2IC0wLjA4OSwwLjQwMiAtMC4wOTUsMC41MzJjLTAuMDEsMC4xMyAtMC4wMTUsMC4yMDUgLTAuMDE1LDAuMjA1bDAuMDA5LDAuMzNsMC4wMDEsMC4wMDdsMi40NzEsMjkuNzE0bDAuMDAxLDAuMDA4YzAsMCAwLjAxLDAuMDc1IDAuMDI3LDAuMjA4YzAuMDE4LDAuMTMxIDAuMDU1LDAuMzIgMC4xMjgsMC41MzdjMC4wNjUsMC4yMiAwLjE4NCwwLjQ2MiAwLjMyOCwwLjcwOWMwLjE1NCwwLjI0MiAwLjM0OCwwLjQ3OSAwLjU3MSwwLjY4OWMwLjIyNywwLjIwNCAwLjQ4MSwwLjM3NiAwLjczNCwwLjUxYzAuMjU5LDAuMTIyIDAuNTA5LDAuMjIgMC43MzQsMC4yNjZjMC4yMjMsMC4wNTMgMC40MTQsMC4wNzYgMC41NDcsMC4wODFjMC4xMzMsMC4wMDcgMC4yMDksMC4wMSAwLjIwOSwwLjAxbDAuMDA3LDBsMTMuMTc3LDBsMC4wMDksMGMwLDAgMC4xMTYsMC4wMDIgMC4yODksLTAuMDIxYzAuMTczLC0wLjAyMSAwLjQsLTAuMDc5IDAuNjExLC0wLjE3NWMwLjIxMiwtMC4wOTUgMC40MDUsLTAuMjI3IDAuNTM2LC0wLjM0MmMwLjEzMSwtMC4xMTQgMC4yMDYsLTAuMjA0IDAuMjA2LC0wLjIwNGwwLjAwNSwtMC4wMDVsMS4wMDcsLTEuMTM2bDAuMDA0LC0wLjAwNWMwLDAgMC4yNDgsLTAuMjg5IDAuNTk2LC0wLjQzOGMwLjM0MiwtMC4xNjIgMC43MjMsLTAuMTU1IDAuNzIzLC0wLjE1NWwzLjk3OCwwbDAsLTAuNDIyYy0wLjE0MSwwLjE0MSAtMC4yODEsMC4yODEgLTAuNDIyLDAuNDIybDMuNTUsMGwwLjAwNiwwYzAsMCAwLjM4MiwtMC4wMDcgMC43MjMsMC4xNTVjMC4zNDksMC4xNDkgMC41OTcsMC40MzggMC41OTcsMC40MzhsMC4wMDQsMC4wMDVsMS4wMDYsMS4xMzZsMC4wMDUsMC4wMDVjMCwwIDAuMDc2LDAuMDkgMC4yMDcsMC4yMDRjMC4xMywwLjExNSAwLjMyNCwwLjI0NyAwLjUzNiwwLjM0MmMwLjIxMSwwLjA5NiAwLjQzOCwwLjE1NCAwLjYxMSwwLjE3NWMwLjE3MiwwLjAyMyAwLjI4OSwwLjAyMSAwLjI4OSwwLjAyMWwxMy4xOTIsMGMwLDAgMC4wNzYsLTAuMDA0IDAuMjEsLTAuMDFjMC4xMzMsLTAuMDA1IDAuMzI0LC0wLjAyOCAwLjU0NiwtMC4wODFjMC4yMjYsLTAuMDQ2IDAuNDc2LC0wLjE0NCAwLjczNCwtMC4yNjZjMC4yNTQsLTAuMTM0IDAuNTA4LC0wLjMwNiAwLjczNSwtMC41MTFjMC4yMjMsLTAuMjA5IDAuNDE3LC0wLjQ0NiAwLjU3MSwtMC42ODhjMC4xNDQsLTAuMjQ3IDAuMjYyLC0wLjQ4OSAwLjMyNywtMC43MDljMC4wNzQsLTAuMjE3IDAuMTExLC0wLjQwNiAwLjEyOCwtMC41MzdjMC4wMTcsLTAuMTMzIDAuMDI3LC0wLjIwOCAwLjAyNywtMC4yMDhsMC4wMDEsLTAuMDA4bDIuNDcyLC0yOS43MTRsMCwtMC4wMDdsMC4wMSwtMC4zMzFjLTAuMDIzLDAuMDA4IDAuMDUyLC0wLjQxOCAtMC4yNTksLTEuMTU5Yy0wLjE2LC0wLjM2MiAtMC40MjksLTAuOCAtMC44OTksLTEuMTcyYy0wLjQ1NiwtMC4zOCAtMS4xMzYsLTAuNjc2IC0xLjkzMywtMC42ODRsMCwwLjg0MmMwLjU4MiwwLjAwOSAxLjA3MywwLjIxOSAxLjQwNSwwLjQ5OGMwLjM0NCwwLjI3MSAwLjUzOSwwLjU5MSAwLjY1NSwwLjg1NGMwLjIyOCwwLjUzNyAwLjE3MSwwLjg0NyAwLjE4OSwwLjg0MWwtMC4wMDcsMC4yNDJsLTAuMDAxLDAuMDA2bC0yLjQ3MSwyOS43MTRsLTAuMDAxLDAuMDA1YzAsMCAtMC4wMDgsMC4wNTcgLTAuMDIxLDAuMTU1Yy0wLjAxMiwwLjA5OSAtMC4wNCwwLjIzOSAtMC4wOTUsMC40Yy0wLjA0NiwwLjE2NSAtMC4xMzcsMC4zNDMgLTAuMjQzLDAuNTI3Yy0wLjExNSwwLjE3OSAtMC4yNTksMC4zNTYgLTAuNDI1LDAuNTEzYy0wLjE2OSwwLjE1MiAtMC4zNTksMC4yOCAtMC41NDcsMC4zOGMtMC4xOTMsMC4wODkgLTAuMzc4LDAuMTY1IC0wLjU0NiwwLjE5N2MtMC4xNjUsMC4wNCAtMC4zMDcsMC4wNTggLTAuNDA2LDAuMDYxYy0wLjA5OSwwLjAwNSAtMC4xNTYsMC4wMDggLTAuMTU2LDAuMDA4bC0wLjAwNywwbC0xMy4xNzcsMGwtMC4wMDUsMGMwLDAgLTAuMjk0LDAuMDA0IC0wLjU1NywtMC4xMjFjLTAuMjY4LC0wLjExNSAtMC40NTksLTAuMzM4IC0wLjQ1OSwtMC4zMzhsLTAuMDA0LC0wLjAwNWwtMS4wMDcsLTEuMTM2bC0wLjAwNSwtMC4wMDVjMCwwIC0wLjA4OSwtMC4xMDYgLTAuMjQ1LC0wLjI0MWMtMC4xNTUsLTAuMTM2IC0wLjM4NCwtMC4yOTMgLTAuNjM1LC0wLjQwNmMtMC4yNTEsLTAuMTEzIC0wLjUyLC0wLjE4MSAtMC43MjUsLTAuMjA2Yy0wLjIwNCwtMC4wMjcgLTAuMzQyLC0wLjAyNCAtMC4zNDIsLTAuMDI0bC0zLjk3OSwwbDAsMC40MjFjMC4xNDEsLTAuMTQgMC4yODEsLTAuMjggMC40MjEsLTAuNDIxbC0zLjU0OSwwbC0wLjAwOCwwLjAwMWMwLDAgLTAuMTM4LC0wLjAwNCAtMC4zNDMsMC4wMjNjLTAuMjA0LDAuMDI1IC0wLjQ3MywwLjA5MyAtMC43MjQsMC4yMDZjLTAuMjUxLDAuMTEzIC0wLjQ4MSwwLjI3IC0wLjYzNSwwLjQwNmMtMC4xNTYsMC4xMzUgLTAuMjQ1LDAuMjQxIC0wLjI0NSwwLjI0MWwtMC4wMDYsMC4wMDVsLTEuMDA2LDEuMTM2bC0wLjAwNCwwLjAwNWMwLDAgLTAuMTkxLDAuMjI0IC0wLjQ1OSwwLjMzOGMtMC4yNjQsMC4xMjUgLTAuNTU3LDAuMTIxIC0wLjU1NywwLjEyMWwtMTMuMTg5LDBjMCwwIC0wLjA1NywtMC4wMDMgLTAuMTU2LC0wLjAwOGMtMC4xLC0wLjAwMyAtMC4yNDIsLTAuMDIxIC0wLjQwNywtMC4wNjFjLTAuMTY4LC0wLjAzMiAtMC4zNTIsLTAuMTA4IC0wLjU0NiwtMC4xOTdjLTAuMTg4LC0wLjEgLTAuMzc3LC0wLjIyOCAtMC41NDcsLTAuMzhjLTAuMTY1LC0wLjE1NiAtMC4zMDksLTAuMzMzIC0wLjQyNSwtMC41MTNjLTAuMTA1LC0wLjE4NCAtMC4xOTYsLTAuMzYyIC0wLjI0MywtMC41MjdjLTAuMDU0LC0wLjE2MSAtMC4wODIsLTAuMzAxIC0wLjA5NSwtMC4zOTljLTAuMDEzLC0wLjA5OSAtMC4wMjEsLTAuMTU1IC0wLjAyMSwtMC4xNTVsMCwtMC4wMDZsLTIuNDcyLC0yOS43MTRsMCwtMC4wMDZsLTAuMDA3LC0wLjI0MWMwLDAgMC4wMDQsLTAuMDU1IDAuMDExLC0wLjE0OWMwLjAwNCwtMC4wOTUgMC4wMjksLTAuMjI5IDAuMDY5LC0wLjM4NmMwLjA4NCwtMC4zMTIgMC4yOCwtMC43MTcgMC42LC0xLjAxNmMwLjMwNSwtMC4zMTQgMC43MTYsLTAuNDk3IDEuMDI5LC0wLjU3NGMwLjE1NywtMC4wMzYgMC4yOTIsLTAuMDU5IDAuMzg4LC0wLjA2YzAuMDk1LC0wLjAwNSAwLjE0OSwtMC4wMDggMC4xNDksLTAuMDA4bDAuMDA2LC0wLjAwMWwxNS41ODksMGwwLjAwNSwwLjAwMWMwLDAgMC4zMDksLTAuMDA1IDAuNTk0LDAuMTA4YzAuMjg4LDAuMTAzIDAuNTE3LDAuMzEgMC41MTcsMC4zMWwwLjAwNCwwLjAwNGwxLjU2NiwxLjM2OWwwLjAwNiwwLjAwNmMwLDAgMC4zMTMsMC4yODIgMC43MDksMC40MjJjMC4zOSwwLjE1NSAwLjgxMiwwLjE0OSAwLjgxMiwwLjE0OWwwLjAwOCwwbDQuMDA0LDBsMCwtMC40MjFjLTAuMTQxLDAuMTQgLTAuMjgxLDAuMjggLTAuNDIyLDAuNDIxbDMuNTkxLDBjMCwwIDAuNDIyLDAuMDA2IDAuODEyLC0wLjE0OWMwLjM5NSwtMC4xNCAwLjcwOSwtMC40MjMgMC43MDksLTAuNDIzbDAuMDA2LC0wLjAwNWwxLjU2NSwtMS4zNjlsMC4wMDUsLTAuMDA0YzAsMCAwLjIyOSwtMC4yMDcgMC41MTcsLTAuMzFjMC4yODUsLTAuMTEzIDAuNTkzLC0wLjEwOSAwLjU5MywtMC4xMDlsMTUuNTk4LDBaIiBzdHlsZT0iZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjwvZz48L2c+PGc+PHBhdGggZD0iTTU1LjU4MywzOS40MDZjLTAuMTI1LDEuNTAyIC0xLjM4LDIuNjU4IC0yLjg4OCwyLjY1OGwtMTMuMzAxLDBjLTAuNTE2LDAgLTEuMDA3LC0wLjIyMSAtMS4zNDksLTAuNjA3bC0xLjAyLC0xLjE1MmMtMC40MiwtMC40NzMgLTEuMDIzLC0wLjc0NCAtMS42NTYsLTAuNzQ0bC03LjE2OSwwYy0wLjYzMywwIC0xLjIzNiwwLjI3MSAtMS42NTYsMC43NDRsLTEuMDIsMS4xNTJjLTAuMzQyLDAuMzg2IC0wLjgzMywwLjYwNyAtMS4zNDksMC42MDdsLTEzLjMwMiwwYy0xLjUwNywwIC0yLjc2MiwtMS4xNTUgLTIuODg3LC0yLjY1N2wtMi40OTUsLTI5Ljk4NWMtMC4xMywtMS41NzIgMS4xMSwtMi45MTkgMi42ODcsLTIuOTE5bDE1LjczMywwYzAuNTE4LDAgMS4wMTcsMC4xODggMS40MDcsMC41MjlsMS41ODQsMS4zODZjMC4zNSwwLjMwNiAwLjc5OSwwLjQ3NSAxLjI2NCwwLjQ3NWw3LjIzNiwwYzAuNDY1LDAgMC45MTUsLTAuMTY5IDEuMjY0LC0wLjQ3NWwxLjU4NSwtMS4zODZjMC4zODksLTAuMzQxIDAuODg5LC0wLjUyOSAxLjQwNiwtMC41MjlsMTUuNzM0LDBjMS41NzcsMCAyLjgxNywxLjM0NyAyLjY4NiwyLjkxOWwtMi40OTQsMjkuOTg0Wm0wLjQzOSwtMzQuMjY5bC00OC40NzUsMGMtMi4wMzYsMCAtMy42MzcsMS43NCAtMy40NjgsMy43N2wyLjU2OSwzMC44OTFjMC4xNywyLjA1MyAxLjg4NiwzLjYzMiAzLjk0NiwzLjYzMmw0Mi4zOCwwYzIuMDYsMCAzLjc3NywtMS41OCAzLjk0NywtMy42MzJsMi41NjksLTMwLjg5MWMwLjE2OSwtMi4wMyAtMS40MzMsLTMuNzcgLTMuNDY4LC0zLjc3IiBzdHlsZT0iZmlsbDojMmUxMDUyO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48Zz48cGF0aCBkPSJNMTcuMDg1LDU5Ljk2Yy0wLjY0NCwwIC0xLjIsLTAuMTM2IC0xLjY3LC0wLjQzM2MtMC40NywtMC4yODQgLTAuODI5LC0wLjY5MiAtMS4wNzYsLTEuMjI0Yy0wLjI2LC0wLjUzMiAtMC4zODMsLTEuMTYyIC0wLjM4MywtMS44OGMwLC0wLjcyOSAwLjEyMywtMS4zNiAwLjM4MywtMS44OTJjMC4yNiwtMC41MzIgMC42MTgsLTAuOTQgMS4wODgsLTEuMjM3YzAuNDcsLTAuMjg0IDEuMDM5LC0wLjQzMyAxLjY4MiwtMC40MzNjMC42NDMsMCAxLjIsMC4xMzcgMS42NTcsMC40MjFjMC40NTgsMC4yODQgMC44MTcsMC42OTMgMS4wNzYsMS4yMjRjMC4yNDgsMC41MiAwLjM3MSwxLjE1IDAuMzcxLDEuODhjMCwwLjczIC0wLjEyMywxLjM3MyAtMC4zODMsMS45MDVjLTAuMjQ3LDAuNTMyIC0wLjYxOCwwLjk1MiAtMS4wNzYsMS4yMzZjLTAuNDcsMC4yODUgLTEuMDI2LDAuNDMzIC0xLjY2OSwwLjQzM1ptMCwtMS40MzRjMC40MiwwIDAuNzQyLC0wLjE4NiAwLjk4OSwtMC41NjljMC4yNDcsLTAuMzg0IDAuMzcxLC0wLjkwMyAwLjM3MSwtMS41NDZjMCwtMC42NTYgLTAuMTI0LC0xLjE2MyAtMC4zNzEsLTEuNTQ2Yy0wLjI0NywtMC4zODMgLTAuNTY5LC0wLjU2OSAtMC45ODksLTAuNTY5Yy0wLjQyMSwwIC0wLjc0MywwLjE5OCAtMC45OSwwLjU2OWMtMC4yNDcsMC4zODMgLTAuMzcxLDAuOTAzIC0wLjM3MSwxLjU0NmMwLDAuNjQzIDAuMTI0LDEuMTYyIDAuMzcxLDEuNTQ2YzAuMjQ3LDAuMzgzIDAuNTY5LDAuNTY5IDAuOTksMC41NjlaIiBzdHlsZT0iZmlsbDojMGQwZDBkO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNMjIuMjY3LDUzLjAyMmwxLjU5NSwwbDAuMTc0LDEuMDUxYzAuMTczLC0wLjM4MyAwLjQzMiwtMC42OCAwLjc5MSwtMC44OWMwLjM1OSwtMC4yMSAwLjc3OSwtMC4zMjIgMS4yNDksLTAuMzIyYzAuNDk1LDAgMC45NTIsMC4xMTIgMS4zODUsMC4zNDdsMCwxLjY4MmMtMC4zOTUsLTAuMzU5IC0wLjkwMiwtMC41MzIgLTEuNTMzLC0wLjUzMmMtMC41MiwwIC0wLjkyOCwwLjExMSAtMS4yNDksMC4zNDZjLTAuMzIyLDAuMjM1IC0wLjUyLDAuNTY5IC0wLjU5NCwxLjAxNGMtMC4wMTIsMC4wOTkgLTAuMDM3LDAuMjM1IC0wLjAzNywwLjM4NGMtMC4wMTIsMC4xNDggLTAuMDEyLDAuMzM0IC0wLjAxMiwwLjU2OWwwLDMuMTE2bC0xLjc2OSwwbDAsLTYuNzY1WiIgc3R5bGU9ImZpbGw6IzBkMGQwZDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTMwLjk3NCw1Mi4yMThjLTAuMTI0LDAgLTAuMTczLC0wLjA2MSAtMC4xNzMsLTAuMTczbDAsLTEuNzA2YzAsLTAuMTI0IDAuMDQ5LC0wLjE4NiAwLjE3MywtMC4xODZsMS40MSwwYzAuMTI0LDAgMC4xODYsMC4wNjIgMC4xODYsMC4xODZsMCwxLjcwNmMwLDAuMTEyIC0wLjA2MiwwLjE3MyAtMC4xODYsMC4xNzNsLTEuNDEsMFptMi4xNzcsNy42NDNjLTAuODI5LDAgLTEuNDM1LC0wLjIyMiAtMS44MDYsLTAuNjY3Yy0wLjM3MSwtMC40MzMgLTAuNTQ0LC0xLjE1MSAtMC41NDQsLTIuMTUybDAsLTIuNjU5bC0xLjQzNSwwbDAsLTEuMzYxbDMuMjA0LDBsMCw0LjAyYzAsMC41MTkgMC4wNzQsMC44OSAwLjI0NywxLjEyNWMwLjE2MSwwLjIyMyAwLjQ0NSwwLjMzNCAwLjg0MSwwLjMzNGwxLjQxLDBsMCwxLjM2bC0xLjkxNywwWiIgc3R5bGU9ImZpbGw6IzBkMGQwZDtmaWxsLXJ1bGU6bm9uemVybzsiLz48L2c+PGc+PHBhdGggZD0iTTM5LjQyMiw1OS45NmMtMC42NDMsMCAtMS4yLC0wLjEzNiAtMS42NywtMC40MzNjLTAuNDY5LC0wLjI4NCAtMC44MjgsLTAuNjkyIC0xLjA3NSwtMS4yMjRjLTAuMjYsLTAuNTMyIC0wLjM4NCwtMS4xNjIgLTAuMzg0LC0xLjg4YzAsLTAuNzI5IDAuMTI0LC0xLjM2IDAuMzg0LC0xLjg5MmMwLjI1OSwtMC41MzIgMC42MTgsLTAuOTQgMS4wODgsLTEuMjM3YzAuNDcsLTAuMjg0IDEuMDM5LC0wLjQzMyAxLjY4MiwtMC40MzNjMC42NDMsMCAxLjE5OSwwLjEzNyAxLjY1NywwLjQyMWMwLjQ1OCwwLjI4NCAwLjgxNiwwLjY5MyAxLjA3NiwxLjIyNGMwLjI0NywwLjUyIDAuMzcxLDEuMTUgMC4zNzEsMS44OGMwLDAuNzMgLTAuMTI0LDEuMzczIC0wLjM4MywxLjkwNWMtMC4yNDgsMC41MzIgLTAuNjE5LDAuOTUyIC0xLjA3NiwxLjIzNmMtMC40NywwLjI4NSAtMS4wMjcsMC40MzMgLTEuNjcsMC40MzNabTAsLTEuNDM0YzAuNDIxLDAgMC43NDIsLTAuMTg2IDAuOTg5LC0wLjU2OWMwLjI0OCwtMC4zODQgMC4zNzEsLTAuOTAzIDAuMzcxLC0xLjU0NmMwLC0wLjY1NiAtMC4xMjMsLTEuMTYzIC0wLjM3MSwtMS41NDZjLTAuMjQ3LC0wLjM4MyAtMC41NjgsLTAuNTY5IC0wLjk4OSwtMC41NjljLTAuNDIsMCAtMC43NDIsMC4xOTggLTAuOTg5LDAuNTY5Yy0wLjI0OCwwLjM4MyAtMC4zNzEsMC45MDMgLTAuMzcxLDEuNTQ2YzAsMC42NDMgMC4xMjMsMS4xNjIgMC4zNzEsMS41NDZjMC4yNDcsMC4zODMgMC41NjksMC41NjkgMC45ODksMC41NjlaIiBzdHlsZT0iZmlsbDojMGQwZDBkO2ZpbGwtcnVsZTpub256ZXJvOyIvPjwvZz48Zz48cGF0aCBkPSJNNDQuMTg0LDUzLjAyMmwxLjU4MywwbDAuMTczLDEuMDE0YzAuMTIzLC0wLjM3MSAwLjMzNCwtMC42NjcgMC42NDMsLTAuODY1YzAuMzA5LC0wLjIxMSAwLjY4LC0wLjMxIDEuMTEzLC0wLjMxYzAuNjE4LDAgMS4xMDEsMC4yMTEgMS40MjIsMC42NDRjMC4zMzQsMC40MzIgMC40OTUsMS4wNjMgMC40OTUsMS44OTJsMCw0LjM5bC0xLjc1NiwwbDAsLTQuMTE4YzAsLTAuNDcgLTAuMDc1LC0wLjgxNiAtMC4xOTgsLTEuMDM5Yy0wLjEzNiwtMC4yMjMgLTAuMzQ3LC0wLjMzNCAtMC42MzEsLTAuMzM0Yy0wLjM0NiwwIC0wLjYwNiwwLjE0OCAtMC44MDQsMC40MzNjLTAuMTg1LDAuMjk3IC0wLjI4NCwwLjY5MyAtMC4yODQsMS4xODdsMCwzLjg3MWwtMS43NTYsMGwwLC02Ljc2NVoiIHN0eWxlPSJmaWxsOiMwZDBkMGQ7ZmlsbC1ydWxlOm5vbnplcm87Ii8+PC9nPjwvZz48L2c+PGcgaWQ9IlRleHQiPjwvZz48L3N2Zz4=" 8 | ) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/orion/handler.cr: -------------------------------------------------------------------------------- 1 | module Orion::Handler 2 | include HTTP::Handler 3 | alias HandlerProc = Server::Context -> 4 | 5 | abstract def call(context : Server::Context) 6 | 7 | def call(context : HTTP::Server::Context) 8 | raise "Cannot use an orion handler with a standard HTTP router." 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/orion/handlers.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | require "./helpers/*" 3 | require "./handlers/*" 4 | -------------------------------------------------------------------------------- /src/orion/handlers/auto_close.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Orion::Handlers::AutoClose 3 | include Handler 4 | 5 | def call(cxt : Server::Context) 6 | call_next cxt 7 | cxt.response.close unless cxt.response.closed? 8 | rescue 9 | # everything is fine, dont raise an error 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/orion/handlers/auto_mime.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Orion::Handlers::AutoMime 3 | include Handler 4 | include MIMEHelper 5 | 6 | def call(cxt : Server::Context) 7 | cxt.request.headers["Accept"] ||= type_from_path?(cxt.request) || "*/*" 8 | call_next(cxt) 9 | if (content_type = request_mime_types(cxt.request).first?) 10 | cxt.response.headers["Content-Type"] ||= content_type 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/orion/handlers/config.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Orion::Handlers::Config 3 | include Handler 4 | @config : Orion::Config 5 | 6 | def initialize(@config : Orion::Config) 7 | end 8 | 9 | def call(cxt : Server::Context) 10 | cxt.config = @config.readonly 11 | call_next cxt 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/orion/handlers/exceptions.cr: -------------------------------------------------------------------------------- 1 | require "ecr" 2 | 3 | class Orion::Handlers::Exceptions 4 | include Handler 5 | 6 | def call(cxt : Server::Context) 7 | begin 8 | call_next(cxt) 9 | rescue error 10 | Log.for(Orion).error(exception: error) { error.class.name } 11 | cxt.response.reset 12 | {% if flag?(:exceptionpage) || (flag?(:release) && !flag?(:noexceptionpage)) %} 13 | release_response cxt, error 14 | {% else %} 15 | dev_response cxt, error 16 | {% end %} 17 | end 18 | end 19 | 20 | private def release_response(cxt : Orion::Server::Context, error : Exception) 21 | status_code, message, subtext = response_for(error) 22 | cxt.response.status_code = status_code 23 | page_title = "#{message} (#{cxt.response.status_code})" 24 | cxt.response.headers["Content-Type"] = "text/html" 25 | ECR.embed "#{__DIR__}/../error_page.html.ecr", cxt.response 26 | end 27 | 28 | private def dev_response(cxt : Orion::Server::Context, error : Exception) 29 | status_code, message, subtext = response_for(error) 30 | cxt.response.status_code = status_code 31 | cxt.response.print ExceptionPage.for_runtime_exception(cxt, error).to_s 32 | end 33 | 34 | private def response_for(error : RoutingError) 35 | {404, "The page you were looking for doesn't exist.", "You may have mistyped the address or the page may have moved."} 36 | end 37 | 38 | private def response_for(error : Exception) 39 | {500, "We're sorry, but something went wrong.", "Please report this error to the application owner for assistance."} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/orion/handlers/logger.cr: -------------------------------------------------------------------------------- 1 | require "uuid" 2 | 3 | class Orion::Handlers::Logger 4 | include Handler 5 | 6 | def initialize(@log = Log.for("http.server")) 7 | end 8 | 9 | def call(context) 10 | Log.with_context do 11 | start = Time.monotonic 12 | request_id = UUID.random.to_s 13 | Log.context.set(operation: {id: request_id}) 14 | begin 15 | call_next(context) 16 | ensure 17 | elapsed = Time.monotonic - start 18 | elapsed_text = elapsed_text(elapsed) 19 | 20 | req = context.request 21 | res = context.response 22 | 23 | addr = 24 | {% begin %} 25 | case remote_address = req.remote_address 26 | when nil 27 | "-" 28 | {% unless flag?(:win32) %} 29 | when Socket::IPAddress 30 | remote_address.address 31 | {% end %} 32 | else 33 | remote_address 34 | end 35 | {% end %} 36 | @log.with_context do 37 | @log.context.set( 38 | http_request: { 39 | "requestMethod" => req.method, 40 | "requestUrl" => URI.new(host: req.headers["Host"], path: req.resource, query: req.query_params).to_s, 41 | "userAgent" => req.headers["User-Agent"], 42 | "remoteIp" => req.remote_address.as?(Socket::IPAddress).try(&.address), 43 | "status" => res.status_code, 44 | "latency" => "#{elapsed.total_seconds}s", 45 | }, 46 | ) 47 | @log.info { "#{addr} - #{req.method} #{req.resource} #{req.version} - #{res.status_code} (#{elapsed_text})" } 48 | end 49 | end 50 | end 51 | end 52 | 53 | private def elapsed_text(elapsed) 54 | minutes = elapsed.total_minutes 55 | return "#{minutes.round(2)}m" if minutes >= 1 56 | 57 | "#{elapsed.total_seconds.humanize(precision: 2, significant: false)}s" 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /src/orion/handlers/method_override_header.cr: -------------------------------------------------------------------------------- 1 | require "http/server" 2 | 3 | # :nodoc: 4 | class Orion::Handlers::MethodOverrideHeader 5 | include Handler 6 | 7 | def call(cxt : Server::Context) 8 | override_method = 9 | cxt.request.headers["x-http-method-override"]? || 10 | cxt.request.headers["x-method-override"]? || 11 | cxt.request.headers["x-http-method"]? 12 | cxt.request.method = override_method if override_method 13 | call_next cxt 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/orion/handlers/method_override_param.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Orion::Handlers::MethodOverrideParam 3 | include Handler 4 | 5 | def call(cxt : Server::Context) 6 | request = cxt.request 7 | override_method = param_method?(request) || form_method?(request) 8 | request.method = override_method if override_method 9 | call_next cxt 10 | end 11 | 12 | private def param_method?(req : HTTP::Request) 13 | req.query_params["_method"]? 14 | end 15 | 16 | private def form_method?(req : HTTP::Request) 17 | if type_for_request(req) == "multipart/form-data" 18 | HTTP::FormData.parse(req) do |part| 19 | if part.name == "_method" 20 | return part.body.gets_to_end 21 | end 22 | end 23 | nil 24 | end 25 | end 26 | 27 | def type_for_request(request : HTTP::Request) 28 | content_type = request.headers["content-type"]?.to_s.split(';').first?.to_s 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /src/orion/handlers/reset_path.cr: -------------------------------------------------------------------------------- 1 | class Orion::Handlers::ResetPath 2 | include Handler 3 | 4 | def call(cxt : Server::Context) 5 | cxt.request.path = cxt.request.path.sub(/^#{cxt.request.base_path}/, "") 6 | call_next(cxt) 7 | cxt.request.reset_path! 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/orion/handlers/route_finder.cr: -------------------------------------------------------------------------------- 1 | class Orion::Handlers::RouteFinder 2 | include Handler 3 | 4 | @tree : DSL::Tree 5 | @strip_extension : Bool 6 | 7 | def initialize(@tree : DSL::Tree, *, @strip_extension = false) 8 | end 9 | 10 | def call(cxt : Server::Context) 11 | action = nil 12 | path = cxt.request.path 13 | @tree.search(@strip_extension ? path.rchop(File.extname(path)) : path) do |result| 14 | unless action 15 | cxt.request.path_params = result.params 16 | action = result.payloads.find &.matches_constraints? cxt.request 17 | action.try &.call(cxt) 18 | end 19 | end 20 | 21 | unless action 22 | raise RoutingError.new("No route matches [#{cxt.request.method}] \"#{cxt.request.path}\"") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/orion/handlers/scope_base_path.cr: -------------------------------------------------------------------------------- 1 | class Orion::Handlers::ScopeBasePath 2 | include Handler 3 | 4 | def initialize(@base_path : String) 5 | end 6 | 7 | def call(cxt : Orion::Server::Context) 8 | cxt.request.base_path = @base_path 9 | call_next cxt 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/orion/helpers.cr: -------------------------------------------------------------------------------- 1 | require "./helpers/*" 2 | -------------------------------------------------------------------------------- /src/orion/helpers/mime_helper.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Orion::MIMEHelper 3 | extend self 4 | 5 | def request_mime_types(req : HTTP::Request) : Set(String) 6 | path_type = type_from_path?(req) 7 | accept_types = types_from_accept(req) 8 | path_type ? [path_type].to_set + accept_types : accept_types 9 | end 10 | 11 | def request_extensions(req : HTTP::Request) : Set(String) 12 | extensions = request_mime_types(req).reduce([] of String) do |exts, mime_type| 13 | exts.concat MIME.extensions(mime_type) 14 | end 15 | (extensions.empty? ? MIME.extensions("text/html") : extensions).to_set 16 | end 17 | 18 | private def types_from_accept(req : HTTP::Request) : Set(String) 19 | if (req.headers["Accept"]?) 20 | req.headers["Accept"]?.to_s.split(",").map do |type| 21 | parts = type.split(";") 22 | type = parts.shift 23 | weight = parts.find(&.starts_with?("q=")).try(&.sub(/^q=/, "").to_f?) || 1.0 24 | {type, weight} 25 | end.sort do |l, r| 26 | r[1] <=> l[1] 27 | end.map(&.first).to_set 28 | else 29 | Set(String).new 30 | end 31 | end 32 | 33 | private def type_accepted?(req, type : String) : Bool 34 | types_from_accept?(req).any? do |req_type| 35 | req_type, req_subtype = req_type.split("/") 36 | match_type, match_subtype = type.split("/") 37 | case {req_type, req_subtype} 38 | when {"*", "*"} 39 | true 40 | when {match_type, "*"} 41 | true 42 | when {match_type, match_subtype} 43 | true 44 | else 45 | false 46 | end 47 | end 48 | end 49 | 50 | private def type_from_path?(req : HTTP::Request) : String? 51 | return if File.extname(req.path).empty? 52 | mime_type = MIME.from_filename(req.path.not_nil!) 53 | mime_type.to_s if mime_type 54 | rescue 55 | nil 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/orion/inflector/controllerize.cr: -------------------------------------------------------------------------------- 1 | require "inflector" 2 | 3 | if ARGV[0][0].ascii_uppercase? 4 | print ARGV[0] + "Controller" 5 | else 6 | print Inflector.camelize(ARGV[0]) + "Controller" 7 | end 8 | -------------------------------------------------------------------------------- /src/orion/inflector/decontrollerize.cr: -------------------------------------------------------------------------------- 1 | require "inflector" 2 | Inflector.underscore(ARGV[0]).rstrip("_controller") 3 | -------------------------------------------------------------------------------- /src/orion/inflector/path_joiner.cr: -------------------------------------------------------------------------------- 1 | base = ARGV[0] 2 | path = ARGV[1] 3 | parts = [base, path].map(&.to_s) 4 | joined_path = String.build do |str| 5 | parts.each_with_index do |part, index| 6 | part.check_no_null_byte 7 | 8 | str << "/" if index > 0 9 | 10 | byte_start = 0 11 | byte_count = part.bytesize 12 | 13 | if index > 0 && part.starts_with?("/") 14 | byte_start += 1 15 | byte_count -= 1 16 | end 17 | 18 | if index != parts.size - 1 && part.ends_with?("/") 19 | byte_count -= 1 20 | end 21 | 22 | str.write part.unsafe_byte_slice(byte_start, byte_count) 23 | end 24 | end 25 | puts joined_path 26 | -------------------------------------------------------------------------------- /src/orion/inflector/pluralize.cr: -------------------------------------------------------------------------------- 1 | require "inflector" 2 | 3 | print Inflector.pluralize(ARGV[0]) 4 | -------------------------------------------------------------------------------- /src/orion/inflector/random_const.cr: -------------------------------------------------------------------------------- 1 | require "random/secure" 2 | require "inflector" 3 | 4 | print "#{ARGV[0]}_#{Random::Secure.hex}" 5 | -------------------------------------------------------------------------------- /src/orion/inflector/singularize.cr: -------------------------------------------------------------------------------- 1 | require "inflector" 2 | 3 | print Inflector.singularize(ARGV[0]) 4 | -------------------------------------------------------------------------------- /src/orion/inflector/underscore.cr: -------------------------------------------------------------------------------- 1 | print ARGV[0].underscore.gsub('-', '_') 2 | -------------------------------------------------------------------------------- /src/orion/pipeline.cr: -------------------------------------------------------------------------------- 1 | require "digest" 2 | 3 | # :nodoc: 4 | struct Orion::Pipeline 5 | CACHE = {} of String => Pipeline 6 | ROUTE_HANDLER = ->(http_context : HTTP::Server::Context) { 7 | context = http_context.as(Server::Context) 8 | context.request.action.try &.invoke(context) 9 | } 10 | 11 | @pipeline : ::HTTP::Handler | ::HTTP::Handler::HandlerProc 12 | @cache_key : String 13 | 14 | def self.new(handlers) 15 | key = cache_key(handlers) 16 | CACHE[key]? || Pipeline.new(handlers, key) 17 | end 18 | 19 | private def self.cache_key(handlers : Array(::HTTP::Handler)) 20 | Digest::MD5.hexdigest do |ctx| 21 | handlers.each do |handler| 22 | ctx.update handler.object_id.to_s 23 | end 24 | end 25 | end 26 | 27 | def initialize(handlers : Array(::HTTP::Handler), @cache_key) 28 | handlers = handlers.map(&.dup) 29 | @pipeline = handlers.empty? ? ROUTE_HANDLER : Server.build_middleware(handlers, ROUTE_HANDLER) 30 | CACHE[cache_key] = self 31 | end 32 | 33 | def call(c : ::HTTP::Server::Context) : Nil 34 | @pipeline.call(c) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/orion/router.cr: -------------------------------------------------------------------------------- 1 | # The `Orion::Router` is the workhorse that does the work when a request comes 2 | # into your application. It will take all of your defined routes and builds you 3 | # an application that can serve HTTP traffic. You can configure the router using 4 | # the `config` in a single file router. Or by calling the `new` or `start` 5 | # method within your app. 6 | struct Orion::Router 7 | @stack : HTTP::Handler 8 | getter handlers = [] of HTTP::Handler 9 | delegate processor, bind, listen, to: @server 10 | delegate call, to: @stack 11 | 12 | def self.start(tree : DSL::Tree, *, config : Config) 13 | new(tree, auto_close: config.autoclose, strip_extension: config.strip_extension).tap do |server| 14 | server.bind(config: config) 15 | server.listen(workers: config.workers) 16 | ::Orion::FLAGS["started"] = true 17 | end 18 | end 19 | 20 | def self.start(tree : DSL::Tree, *, autoclose : Bool = true, strip_extension : Bool = false, workers = nil, **bind_opts) 21 | new(tree, autoclose: autoclose, strip_extension: strip_extension).tap do |server| 22 | server.bind(**bind_opts) 23 | server.listen(workers: workers) 24 | ::Orion::FLAGS["started"] = true 25 | end 26 | end 27 | 28 | def initialize(tree : DSL::Tree, *, autoclose : Bool = true, strip_extension : Bool = false) 29 | use Handlers::AutoClose if autoclose 30 | use Handlers::Exceptions.new 31 | use Handlers::MethodOverrideHeader 32 | use Handlers::AutoMime 33 | use Handlers::RouteFinder.new(tree, strip_extension: strip_extension) 34 | @stack = Server.build_middleware handlers 35 | @server = Server.new(handler: @stack) 36 | end 37 | 38 | # Visualize the route tree 39 | def visualize 40 | tree.visualize 41 | end 42 | 43 | def use(handler : HTTP::Handler) 44 | handlers << handler 45 | end 46 | 47 | def use(handler) 48 | use handler.new 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/orion/server.cr: -------------------------------------------------------------------------------- 1 | class Orion::Server < HTTP::Server 2 | @name : String = File.basename Dir.current 3 | getter processor : ::HTTP::Server::RequestProcessor 4 | 5 | # Creates a new Orion server with the given *handler*. 6 | def initialize(handler : HTTP::Handler | HTTP::Handler::HandlerProc) 7 | @processor = RequestProcessor.new(handler) 8 | end 9 | 10 | # Bind using a URI 11 | def bind(*, tls : Nil = nil, uri) 12 | bind(uri: uri) 13 | end 14 | 15 | # Bind TLS with an address 16 | def bind(*, tls : OpenSSL::SSL::Context::Server, address : ::Socket::IPAddress) 17 | bind_tls(address: address, context: tls) 18 | end 19 | 20 | # Bind TLS with a host and port 21 | def bind(*, tls : OpenSSL::SSL::Context::Server, host = ::Socket::IPAddress::LOOPBACK, port = nil, reuse_port = false) 22 | if port 23 | bind_tls(host: host, port: port, context: tls, reuse_port: reuse_port) 24 | else 25 | bind_tls(host: host, context: tls) 26 | end 27 | end 28 | 29 | # Bind TCP to a host and port 30 | def bind(*, tls : Nil = nil, host = ::Socket::IPAddress::LOOPBACK, port = nil, reuse_port = false) 31 | if port 32 | bind_tcp(host: host, port: port.to_i, reuse_port: reuse_port) 33 | else 34 | bind_unused_port(host: host, reuse_port: reuse_port) 35 | end 36 | end 37 | 38 | # Bind TCP to a Socket::IPAddress 39 | def bind(*, tls : Nil = nil, address : ::Socket::IPAddress, reuse_port = false) 40 | bind_tcp(address: address, reuse_port: reuse_port) 41 | end 42 | 43 | # Bind to a Socket::UnixAddress 44 | def bind(*, tls = nil, address : ::Socket::UNIXAddress) 45 | bind_unix(address: address) 46 | end 47 | 48 | def bind(*, tls = nil, path) 49 | bind_unix(path: path) 50 | end 51 | 52 | # Bind using a config 53 | def bind(*, config : ::Orion::Config) 54 | @name = config.name 55 | case config 56 | when .path 57 | bind(path: config.path.not_nil!) 58 | when .address 59 | bind(tls: config.tls, address: config.address.not_nil!) 60 | else 61 | bind(tls: config.tls, host: config.host.not_nil!, port: config.port, reuse_port: config.reuse_port) 62 | end 63 | end 64 | 65 | # Listen clients using multiple workers 66 | # A good suggestion is to use System.cpu_count 67 | def listen(*args, workers, **opts) 68 | if (workers.nil? || workers <= 1) 69 | listen(*args, **opts) 70 | else 71 | workers.times do |i| 72 | Process.fork do 73 | listen(*args, **opts, worker: i) 74 | end 75 | end 76 | sleep 77 | end 78 | end 79 | 80 | # Listen for clients 81 | private def listen(*args, worker = nil, **opts) 82 | each_address do |address| 83 | listen_message(address, worker ? "#{@name}.#{worker}" : @name) 84 | end 85 | super(*args, **opts) 86 | end 87 | 88 | private def listen_message(socket : ::Socket::UNIXAddress, prefix) 89 | Log.info { "listening on #{socket.path}" } 90 | end 91 | 92 | private def listen_message(socket : ::Socket::IPAddress, prefix) 93 | Log.info { "listening on #{socket.address}:#{socket.port}" } 94 | end 95 | end 96 | 97 | require "./server/*" 98 | -------------------------------------------------------------------------------- /src/orion/server/context.cr: -------------------------------------------------------------------------------- 1 | class Orion::Server::Context < HTTP::Server::Context 2 | getter! config : Orion::Config::ReadOnly? 3 | 4 | # :nodoc: 5 | def initialize(@request : Request, @response : Response) 6 | end 7 | 8 | def config=(config : Orion::Config::ReadOnly) 9 | raise Exception.new("Cannot change the config during a request") if @config 10 | @config = config 11 | end 12 | 13 | def request : Request 14 | @request.as(Request) 15 | end 16 | 17 | def response : Response 18 | @response.as(Response) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/orion/server/request.cr: -------------------------------------------------------------------------------- 1 | class Orion::Server::Request < HTTP::Request 2 | @original_path : String? 3 | 4 | setter path_params : Hash(String, String)? 5 | property base_path : String = "/" 6 | property action : Orion::Action? 7 | 8 | # Returns the list of path params set by an Orion route. 9 | def path_params 10 | @path_params ||= {} of String => String 11 | end 12 | 13 | # The original path of the request 14 | def reset_path! 15 | if (original_path = @original_path) 16 | self.path = original_path 17 | @original_path = nil 18 | end 19 | end 20 | 21 | def path=(new_path) 22 | @original_path = path 23 | super(new_path) 24 | end 25 | 26 | # The format of the http request 27 | def format 28 | formats.first 29 | end 30 | 31 | # The formats of the http request 32 | def formats 33 | MIMEHelper.request_extensions(self).tap do |set| 34 | set << File.extname(resource) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /src/orion/server/request_processor.cr: -------------------------------------------------------------------------------- 1 | require "http" 2 | 3 | class Orion::Server::RequestProcessor < HTTP::Server::RequestProcessor 4 | include HTTP 5 | # Some magic to ensure support for all versions of crystal 6 | {{ HTTP::Server::RequestProcessor.methods.map(&.id.gsub(/HTTP::(Server::)?(Request|Response|Context)/, "::Orion::Server::\\2")).join("\n").id }} 7 | end 8 | -------------------------------------------------------------------------------- /src/orion/server/response.cr: -------------------------------------------------------------------------------- 1 | class Orion::Server::Response < HTTP::Server::Response 2 | end 3 | -------------------------------------------------------------------------------- /src/orion/view.cr: -------------------------------------------------------------------------------- 1 | require "./view/*" 2 | 3 | module Orion::View 4 | @rendered = false 5 | 6 | private macro setup_hooks! 7 | {% if @type.class? || @type.struct? %} 8 | alias ParentRenderer = ::Orion::View::Renderer 9 | class Renderer < ParentRenderer ; end 10 | 11 | private def __renderer__ 12 | Renderer.new(self) 13 | end 14 | 15 | macro inherited 16 | {% verbatim do %} 17 | alias ParentRenderer = {{ @type.superclass }}::Renderer 18 | class Renderer < ParentRenderer ; end 19 | private def __renderer__ 20 | Renderer.new(self) 21 | end 22 | {% end %} 23 | end 24 | {% else %} 25 | macro included 26 | setup_hooks! 27 | end 28 | {% end %} 29 | end 30 | 31 | setup_hooks! 32 | 33 | # Include a helper module in the view 34 | # Use this to add helpers to a view from a module 35 | macro view_helper(mod) 36 | class Renderer < ParentRenderer 37 | include {{ mod }} 38 | end 39 | end 40 | 41 | # A block that can be read by the view. 42 | # Use this to define methods that can be accessed by the view 43 | macro view_helper(&block) 44 | class Renderer < ParentRenderer 45 | {{ yield }} 46 | end 47 | end 48 | 49 | # Define a layout to be used within the controller 50 | macro layout(filename, *, locals = NamedTuple.new) 51 | {% raise "Cannot call layout within a def" if @def %} 52 | {% if filename == false %} 53 | {% layout_token = nil %} 54 | {% else %} 55 | {% layout_file = run("./view/renderer/layout_finder.cr", filename) %} 56 | {% layout_token = run("./view/renderer/tokenize.cr", layout_file) %} 57 | {% end %} 58 | 59 | # Render a view 60 | macro render(*, view = @def.name, layout = true, locals = NamedTuple.new, layout_locals = nil) 61 | \{% raise "Cannot call render outside a def" unless @def %} 62 | render_with_layout({{ layout_token }}, \{{ layout }}, \{{ view }}, {{ locals }}, \{{ layout_locals }}, \{{ locals }}) 63 | end 64 | end 65 | 66 | # No layout by default 67 | layout false 68 | 69 | # :nodoc: 70 | private macro render_with_layout(layout_token, layout_option, view, layout_locals, layout_local_overrides, locals) 71 | raise ::Orion::DoubleRenderError.new("Already rendered, check #{self.class.name}") if @rendered 72 | 73 | # Render the view without a layout 74 | {% if layout_token == nil || layout_option == false || layout_option == nil %} 75 | {% view_file = run("./view/renderer/view_finder.cr", @type.name, view) %} 76 | {% view_token = run("./view/renderer/tokenize.cr", view_file) %} 77 | __renderer__.__template_{{ view_token }}__("", false, {{ locals }}) 78 | @rendered = true 79 | 80 | # Render in a layout with the provided layout token 81 | {% elsif layout_option == true %} 82 | {% view_file = run("./view/renderer/view_finder.cr", @type.name, view) %} 83 | {% view_token = run("./view/renderer/tokenize.cr", view_file) %} 84 | __renderer__.__template_{{ layout_token }}__("", {{ layout_locals }}, {{ locals }}) do 85 | __renderer__.__template_string_{{ view_token }}__("", {{ locals }}, {{ layout_local_overrides }}, {{ layout_locals }}) 86 | end 87 | @rendered = true 88 | 89 | # Generate a new layout token and 90 | {% elsif layout_option.is_a? StringLiteral %} 91 | {% layout_file = run("./view/renderer/layout_finder.cr", filename) %} 92 | {% layout_token = run("./view/renderer/tokenize.cr", layout_file) %} 93 | render_with_layout({{ layout_token }}, true, {{ view }}, {{ layout_locals }}, {{ layout_local_overrides }}, {{ locals }}) 94 | 95 | # Raise if the expected shape is wrong 96 | {% else %} 97 | {% raise "layout must be of type `String | Nil | Bool`" %} 98 | {% end %} 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /src/orion/view/asset_tag_helpers.cr: -------------------------------------------------------------------------------- 1 | require "html_builder" 2 | 3 | # Patch HTML escape function 4 | def HTML.escape(stringable : Bool | Int | Float, *args) 5 | HTML.escape(stringable.to_s, *args) 6 | end 7 | 8 | module Orion::View::AssetTagHelpers 9 | # Returns an HTML image tag for the source. The source can be a full path or a 10 | # file that exists in your assets/images directory. 11 | def image_tag(src : String, **attrs) 12 | HTML.build do 13 | img(**attrs, src: assets_local? ? image_path(src) : image_url(src)) 14 | end 15 | end 16 | 17 | # Computes the path to an image asset in the assets/images directory. 18 | # Full paths from the document root will be passed through. Used internally 19 | # by image_tag to build the image path. 20 | def image_path(src : String) 21 | asset_path(File.expand_path(src, "/images")) 22 | end 23 | 24 | # Computes the URL to an image asset in the assets/images directory. 25 | # This will call image_path internally and merge with your current host or 26 | # your asset host. 27 | def image_url(src : String) 28 | asset_url(File.expand_path(src, "/images")) 29 | end 30 | 31 | # Returns an HTML script tag for each of the sources provided. You can pass in 32 | # the filename (.js extension is optional) of JavaScript files that exist in 33 | # your assets/javascripts directory for inclusion into the current page or 34 | # you can pass the full path relative to your document root. 35 | def javascript_include_tag(src, **attrs) 36 | HTML.build do 37 | script(**attrs, src: assets_local? ? javascript_path(src) : javascript_url(src)) { } 38 | end 39 | end 40 | 41 | # Computes the path to a JavaScript asset in the assets/javascripts 42 | # directory. If the source filename has no extension, .js will be appended. 43 | # Full paths from the document root will be passed through. Used internally 44 | # by javascript_include_tag to build the script path. 45 | def javascript_path(file) 46 | asset_path(File.expand_path(file, "/javascripts"), extname: ".js") 47 | end 48 | 49 | # Computes the URL to a JavaScript asset in the assets/javascripts 50 | # directory. This will call javascript_path internally and merge with your 51 | # current host or your asset host. 52 | def javascript_url(file : String) 53 | asset_url(File.expand_path(file, "/javascripts"), extname: ".js") 54 | end 55 | 56 | # Returns a stylesheet link tag for the sources specified as arguments. If 57 | # you don't specify an extension, .css will be appended automatically. 58 | def stylesheet_link_tag(href, **attrs) 59 | HTML.build do 60 | link(**attrs, rel: "stylesheet", href: assets_local? ? stylesheet_path(href) : stylesheet_url(href)) 61 | end 62 | end 63 | 64 | # Computes the path to a stylesheet asset in the assets/stylesheets 65 | # directory. If the source filename has no extension, .css will be appended. 66 | # Full paths from the document root will be passed through. Used internally by 67 | # stylesheet_link_tag to build the stylesheet path. 68 | def stylesheet_path(file) 69 | asset_path(File.expand_path(file, "/stylesheets"), extname: ".css") 70 | end 71 | 72 | # Computes the URL to a stylesheet asset in the assets/stylesheets 73 | # directory. This will call stylesheet_path internally and merge with your 74 | # current host or your asset host. 75 | def stylesheet_url(file : String) 76 | asset_url(File.expand_path(file, "/javascripts"), extname: ".css") 77 | end 78 | 79 | # Computes the path to an asset in the assets directory. If the source 80 | # filename has no extension, the provided extnam will be appended. 81 | # Full paths from the document root will be passed through. 82 | def asset_path(file, *, extname : String? = nil) 83 | file = extname ? "#{file}#{extname}" : file 84 | File.join("/assets", file) 85 | end 86 | 87 | # Computes the URL to an asset in the assets directory. This will call 88 | # asset_path internally and merge with your current host or your asset host. 89 | def asset_url(file : String, *, extname : String? = nil) 90 | "//#{config.asset_host || request.headers["Host"]?}#{asset_path(file, extname: extname)}" 91 | end 92 | 93 | # Returns a link tag that browsers and feed readers can use to auto-detect an 94 | # RSS, Atom, or JSON feed. 95 | def auto_discovery_link_tag(type : String, href, **attrs) 96 | HTML.build do 97 | link(**attrs, type: MIME.from_extension(".#{type}") { type }, href: href, rel: "alternate") 98 | end 99 | end 100 | 101 | private def assets_local? 102 | config.asset_host.nil? || config.asset_host == request.headers["Host"]? 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /src/orion/view/cache_helpers.cr: -------------------------------------------------------------------------------- 1 | module Orion::View::CacheHelpers 2 | # Cache the block, using the object as the key 3 | macro cache(object, &block) 4 | %orig_cache_key = __cache_key__ 5 | __cache_key__ = extend_cache_key(__cache_key__, {{ object }}) 6 | config.cache.fetch(__cache_key__) do 7 | String.build do |__kilt_io__| 8 | {{ yield }} 9 | end 10 | end.to_s(__kilt_io__) 11 | __cache_key__ = %orig_cache_key 12 | end 13 | 14 | # Cache the block, if the condition is true, using the object as the key 15 | macro cache_if(condition, object, &block) 16 | %orig_cache_key = __cache_key__ 17 | __cache_key__ = extend_cache_key(__cache_key__, {{ object }}) 18 | config.cache.fetch_if({{ condition }}, __cache_key__) do 19 | String.build do |__kilt_io__| 20 | {{ yield }} 21 | end.to_s(__kilt_io__) 22 | end 23 | __cache_key__ = %orig_cache_key 24 | end 25 | 26 | # 27 | private def extend_cache_key(prev_key : String, next_key : String) 28 | "#{prev_key}/#{next_key}" 29 | end 30 | 31 | private def extend_cache_key(prev_key : String, next_key : Cache::Keyable) 32 | "#{prev_key}/#{next_key.cache_key}" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /src/orion/view/capture_helper.cr: -------------------------------------------------------------------------------- 1 | module Orion::View::CaptureHelper 2 | @content_fors : Hash(Symbol, String) 3 | 4 | # The capture macro allows you to extract part of a template into a variable. 5 | # You can then use this variable anywhere in your templates or layout. 6 | macro capture(&block) 7 | String.build do |__kilt_io__| 8 | {{ yield }} 9 | end 10 | end 11 | 12 | macro content_for(name, &block) 13 | @content_fors[{{ name }}] = capture do 14 | {{ yield }} 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/orion/view/partial_helpers.cr: -------------------------------------------------------------------------------- 1 | module Orion::View::PartialHelpers 2 | # Render a view partial 3 | macro render(*, partial, locals = NamedTuple.new) 4 | {% raise "Cannot call render outside a def" unless @def %} 5 | {% partial_file = run("./renderer/partial_finder.cr", @type.name, partial) %} 6 | {% partial_token = run("./renderer/tokenize.cr", partial_file) %} 7 | __template_string_{{ partial_token }}__(__cache_key__, {{ locals }}, __locals__) 8 | end 9 | 10 | # Render a collection into a partial 11 | macro render(*, partial, collection, cached = false) 12 | {% raise "Cannot call render outside a def" unless @def %} 13 | @collection.each do |item| 14 | cache_if({{ cached }}, item) do 15 | render(partial: {{ partial }}, locals: { item: {{ item }} }) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/orion/view/registry.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Orion::View::Registry 3 | {{ run "./renderer/defs.cr", `date +%s%N` }} 4 | 5 | private def combine_locals(left : Nil, right : NamedTuple) 6 | right 7 | end 8 | 9 | private def combine_locals(left : NamedTuple, right : Nil) 10 | left 11 | end 12 | 13 | private def combine_locals(left : NamedTuple, right : NamedTuple) 14 | right.merge(left) 15 | end 16 | 17 | private def combine_locals(l1, l2, l3) 18 | combine_locals(l1, combine_locals(l2, l3)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/orion/view/renderer.cr: -------------------------------------------------------------------------------- 1 | require "../cache" 2 | require "./*" 3 | 4 | class Orion::View::Renderer 5 | include Controller::RequestHelpers 6 | include Registry 7 | include AssetTagHelpers 8 | include PartialHelpers 9 | include CacheHelpers 10 | 11 | getter config : Orion::Config::ReadOnly 12 | getter controller_name : String 13 | getter request : Orion::Server::Request 14 | getter __kilt_io__ : IO 15 | 16 | def initialize(controller : Orion::Controller) 17 | @__kilt_io__ = IO::MultiWriter.new controller.response 18 | @request = controller.request 19 | @config = controller.context.config 20 | @controller_name = controller.__name__ 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/orion/view/renderer/defs.cr: -------------------------------------------------------------------------------- 1 | def tokenize(string) 2 | string.sub(/^\.\/src\/views\//, "").gsub(/[^A-Za-z0-9_]/, "_") 3 | end 4 | 5 | Dir.glob("./src/views/**/*").each do |view| 6 | token = tokenize(view) 7 | puts <<-crystal 8 | 9 | # :nodoc: 10 | def __template_#{token}__(__cache_key__ : String, *locals, &block) 11 | __locals__ = locals = combine_locals(*locals) 12 | Kilt.embed "#{view}" 13 | end 14 | 15 | # :nodoc: 16 | def __template_#{token}__(__cache_key__ : String, *locals) 17 | __locals__ = locals = combine_locals(*locals) 18 | Kilt.embed "#{view}" 19 | end 20 | 21 | # :nodoc: 22 | def __template_string_#{token}__(__cache_key__ : String, *locals) 23 | __locals__ = locals = combine_locals(*locals) 24 | Kilt.render "#{view}" 25 | end 26 | 27 | # :nodoc: 28 | def __template_string_#{token}__(__cache_key__ : String, *locals, &block : Symbol -> String) 29 | __locals__ = locals = combine_locals(*locals) 30 | Kilt.render "#{view}" 31 | end 32 | 33 | crystal 34 | end 35 | -------------------------------------------------------------------------------- /src/orion/view/renderer/layout_finder.cr: -------------------------------------------------------------------------------- 1 | require "inflector" 2 | layout_dir = "src/views/layouts" 3 | filename = ARGV[0] 4 | layout_file = File.join(layout_dir, filename) 5 | 6 | if File.exists? layout_file 7 | print layout_file 8 | else 9 | STDERR.puts "could not find layout: #{layout_file} in (#{layout_dir})" 10 | Process.exit 1 11 | end 12 | -------------------------------------------------------------------------------- /src/orion/view/renderer/partial_finder.cr: -------------------------------------------------------------------------------- 1 | require "inflector" 2 | view_dir = "src/views" 3 | controller_name = ARGV[0] 4 | partial_name = ARGV[1] 5 | 6 | parts = partial_name.split("/") 7 | parts << "_" + parts.pop 8 | filename = parts.join("/") 9 | controller_prefix = Inflector.underscore(controller_name.gsub(/Controller::Renderer$/, "")) 10 | controller_dir = File.join(view_dir, controller_prefix) 11 | controller_file = File.join(controller_dir, filename) 12 | view_file = File.join(view_dir, filename) 13 | 14 | if File.exists? controller_file 15 | print controller_file 16 | elsif File.exists? view_file 17 | print view_file 18 | else 19 | STDERR.puts "could not find partial: #{partial_name} in (#{controller_dir}, #{view_dir}), partial filenames must begin with underscore." 20 | Process.exit 1 21 | end 22 | -------------------------------------------------------------------------------- /src/orion/view/renderer/tokenize.cr: -------------------------------------------------------------------------------- 1 | file = File.expand_path(ARGV[0]) 2 | print "#{file.sub(/^#{Dir.current}\/src\/views\//, "").gsub(/[^A-Za-z0-9_]/, "_")}" 3 | -------------------------------------------------------------------------------- /src/orion/view/renderer/view_finder.cr: -------------------------------------------------------------------------------- 1 | require "inflector" 2 | view_dir = "src/views" 3 | controller_name = ARGV[0] 4 | filename = ARGV[1] 5 | 6 | controller_prefix = Inflector.underscore(controller_name).gsub(/_controller$/, "") 7 | controller_dir = File.join(view_dir, controller_prefix) 8 | controller_file = File.join(controller_dir, filename) 9 | view_file = File.join(view_dir, filename) 10 | 11 | if File.exists? controller_file 12 | print controller_file 13 | elsif File.exists? view_file 14 | print view_file 15 | else 16 | STDERR.puts "could not find view: #{filename} in (#{controller_dir}, #{view_dir})" 17 | Process.exit 1 18 | end 19 | -------------------------------------------------------------------------------- /src/orion/write_tracker.cr: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | class Orion::WriteTracker < IO 3 | getter written : Bool = false 4 | 5 | def read(slice : Bytes) 6 | raise "Cannot read from write tracker" 7 | end 8 | 9 | def write(slice : Bytes) : Nil 10 | return 0i64 if slice.empty? 11 | @written = true 12 | slice.size.to_i64 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/parse_version.cr: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | version = YAML.parse(File.read "shard.yml")["version"].to_s 3 | puts <<-crystal 4 | VERSION = #{version.inspect} 5 | crystal 6 | --------------------------------------------------------------------------------