├── .amber.yml
├── .ameba.yml
├── .crystal-version
├── .dockerignore
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── config
├── application.cr
├── database.yml
├── deploy
│ └── .gitkeep
├── initializers
│ ├── database.cr
│ ├── granite.cr
│ ├── hashids.cr
│ ├── multiauth.cr
│ ├── sidekiq.cr
│ └── site.cr
├── routes.cr
└── site.yml
├── db
├── data
│ ├── announcements.json.gz
│ └── announcements.sql.gz
├── migrate.cr
├── migrations
│ ├── 20170701230806_create_announcement.sql
│ ├── 20170710175120_create_user.sql
│ ├── 20170712100522_add_roles_to_users.sql
│ ├── 20170919173941_change_timestamp_columns.sql
│ └── 20171002221739_add_handle_to_users.sql
├── seeds.cr
└── seeds
│ ├── announcements.cr
│ └── users.cr
├── docker-compose.yml
├── package-lock.json
├── package.json
├── public
├── crossdomain.xml
├── favicon.ico
├── javascripts
│ ├── highlight.pack.js
│ └── main.js
├── launcher-icon
│ ├── launcher-icon-1x.png
│ ├── launcher-icon-2x.png
│ ├── launcher-icon-3x.png
│ ├── launcher-icon-4x.png
│ ├── launcher-icon-5x.png
│ └── launcher-icon-6x.png
├── manifest.json
├── robots.txt
└── stylesheets
│ ├── main.css
│ └── monokai-sublime.min.css
├── shard.lock
├── shard.yml
├── spec
├── controllers
│ ├── announcement_controller_spec.cr
│ ├── error_controller_spec.cr
│ ├── oauth_controller_spec.cr
│ ├── rss_controller_spec.cr
│ ├── session_controller_spec.cr
│ ├── spec_helper.cr
│ ├── static_controller_spec.cr
│ └── user_controller_spec.cr
├── helpers
│ ├── page_title_helper_spec.cr
│ ├── query_helper_spec.cr
│ └── time_ago_helper_spec.cr
├── initializers
│ ├── database_spec.cr
│ └── site_spec.cr
├── models
│ ├── announcement_spec.cr
│ ├── spec_helper.cr
│ └── user_spec.cr
├── spec_helper.cr
├── support
│ ├── factories.cr
│ └── matchers.cr
└── workers
│ └── tweet_announcement_spec.cr
└── src
├── controllers
├── announcement_controller.cr
├── application_controller.cr
├── error_controller.cr
├── oauth_controller.cr
├── rss_controller.cr
├── session_controller.cr
├── static_controller.cr
└── user_controller.cr
├── crystal-ann.cr
├── helpers
├── page_title_helper.cr
├── query_helper.cr
└── time_ago_helper.cr
├── mailers
└── .gitkeep
├── models
├── announcement.cr
└── user.cr
├── sidekiq.cr
├── views
├── announcement
│ ├── _entry.slang
│ ├── _form.slang
│ ├── edit.slang
│ ├── index.slang
│ ├── new.slang
│ └── show.slang
├── layouts
│ ├── _footer.slang
│ ├── _ga.slang
│ ├── _nav.slang
│ ├── _profile.slang
│ ├── _search.slang
│ ├── _social_nav.slang
│ ├── _tags.slang
│ └── application.slang
├── rss
│ └── show.slang
├── shared
│ └── _pagination.slang
├── static
│ └── about.slang
└── user
│ └── show.slang
└── workers
└── tweet_announcement.cr
/.amber.yml:
--------------------------------------------------------------------------------
1 | type: app
2 | database: pg
3 | language: slang
4 |
--------------------------------------------------------------------------------
/.ameba.yml:
--------------------------------------------------------------------------------
1 | # This configuration file was generated by `ameba --gen-config`
2 | # on 2017-12-23 21:39:50 +0200 using Ameba version 0.3.0.
3 | # The point is for the user to remove these configuration records
4 | # one by one as the reported problems are removed from the code base.
5 |
6 | # Problems found: 3
7 | # Run `ameba --only LargeNumbers` for details
8 | LargeNumbers:
9 | Description: Disallows usage of large numbers without underscore
10 | IntMinDigits: 5
11 | Enabled: true
12 | Excluded:
13 | - spec/controllers/oauth_controller_spec.cr
14 |
--------------------------------------------------------------------------------
/.crystal-version:
--------------------------------------------------------------------------------
1 | 0.24.2
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | lib
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /doc/
2 | /libs/
3 | /lib/
4 | /bin/
5 | /.crystal/
6 | /.shards/
7 | .env
8 | crystal-ann
9 |
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | env:
2 | AMBER_ENV=test
3 | MICRATE_RUN_UP=true
4 | DATABASE_NAME=crystal_ann_test
5 | DATABASE_URL=postgres://postgres@localhost/$DATABASE_NAME
6 |
7 | language: crystal
8 | services: postgresql
9 | before_script:
10 | - psql -c "create database $DATABASE_NAME;" -U postgres
11 | install:
12 | - crystal deps
13 | script:
14 | - crystal spec
15 | - bin/ameba
16 |
--------------------------------------------------------------------------------
/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 velenhaupt@gmail.com. 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 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | FROM crystallang/crystal:0.24.1 as builder
3 |
4 | RUN apt-get update
5 |
6 | WORKDIR /app
7 | COPY . /app
8 | RUN shards --production
9 |
10 | RUN mkdir bin && crystal build --release src/crystal-ann.cr -o bin/crystal_ann
11 | RUN chmod +x bin/crystal_ann
12 |
13 | CMD ["bin/crystal_ann"]
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: ./app
2 | worker: ./sidekiq -c 3
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Crystal [ANN]
2 | [](https://github.com/crystal-community/crystal-ann)
3 | [](http://www.amberframework.org/)
4 | [](https://twitter.com/crystallang_ann)
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Notice
12 |
13 | **Crystal [ANN] is no longer maintained. Website was taken down as of March 2025**.
14 |
15 | The announcement data has been exported and preserved in the `db/data` folder:
16 |
17 | - `announcements.json.gz`: JSON format of all announcements
18 | - `announcements.sql.gz`: SQL dump of the announcements table
19 |
20 | This repository remains available for historical reference and educational purposes.
21 |
22 | ## Setup
23 |
24 | 1. [Install Crystal](https://crystal-lang.org/docs/installation/index.html)
25 | 2. [Install Amber Framework](https://docs.amberframework.org/amber/getting-started)
26 | 3. [Install Postgres](http://postgresguide.com/setup/install.html)
27 | 4. Create `crystal_ann` and `crystal_ann_test` pg databases
28 |
29 | ## Development
30 |
31 | 1. Install project dependencies:
32 |
33 | ```
34 | $ shards install
35 | ```
36 |
37 | 2. Run database migrations:
38 |
39 | ```
40 | $ amber db migrate
41 | ```
42 |
43 | 3. Seed data:
44 |
45 | ```
46 | $ amber db seed
47 | ```
48 |
49 | 4. Start app and watch for source changes:
50 |
51 | ```
52 | $ amber w
53 | ```
54 |
55 | ## Testing
56 |
57 | Migrate test database and run specs:
58 |
59 | ```
60 | $ MICRATE_RUN_UP=true crystal spec
61 | ```
62 |
63 | ## Docker
64 |
65 | Run the app using docker-compose
66 |
67 | ``` sh
68 | docker-compose up
69 | ```
70 |
71 | ## Deployment to Heroku
72 |
73 | ```
74 | $ heroku create app-name --buildpack https://github.com/crystal-lang/heroku-buildpack-crystal.git
75 | $ heroku buildpacks:add https://github.com/veelenga/heroku-buildpack-sidekiq.cr
76 | $ git push heroku master
77 | ```
78 |
79 | And set environment variables with `heroku config:set VAR=VAL`:
80 |
81 | ```
82 | AMBER_ENV
83 | AMBER_SESSION_SECRET
84 |
85 | MICRATE_RUN_UP
86 | REDIS_PROVIDER
87 |
88 | GITHUB_ID
89 | GITHUB_SECRET
90 |
91 | TWITTER_CONSUMER_KEY
92 | TWITTER_CONSUMER_SECRET
93 | TWITTER_ACCESS_TOKEN
94 | TWITTER_ACCESS_TOKEN_SECRET
95 |
96 | TWITTER_OAUTH_CONSUMER_KEY
97 | TWITTER_OAUTH_CONSUMER_SECRET
98 | ```
99 |
100 | ## Contributors
101 |
102 | * [veelenga](https://github.com/veelenga) V. Elenhaupt - creator, maintainer
103 | * [hugoabonizio](https://github.com/hugoabonizio) Hugo Abonizio - contributor, maintainer
104 | * [janczer](https://github.com/janczer) Janczer - contributor
105 | * [lex111](https://github.com/lex111) Alexey Pyltsyn - contributor
106 | * [vaibhavsingh97](https://github.com/vaibhavsingh97) Vaibhav Singh - contributor
107 | * [PAPERPANKS](https://github.com/PAPERPANKS) Pankaj Kumar Gautam - contributor
108 |
--------------------------------------------------------------------------------
/config/application.cr:
--------------------------------------------------------------------------------
1 | AMBER_PORT = ENV["PORT"]? || 3008
2 | AMBER_ENV = ENV["AMBER_ENV"]? || "development"
3 |
4 | require "amber"
5 | require "./initializers/*"
6 |
7 | Amber::Server.configure do |settings|
8 | settings.name = "Crystal [ANN] web application."
9 | settings.port = AMBER_PORT.to_i
10 | settings.host = "0.0.0.0"
11 |
12 | settings.session = {
13 | "key" => "crystal.ann.session",
14 | "store" => "encrypted_cookie",
15 | "expires" => 0,
16 | "secret" => ENV["AMBER_SESSION_SECRET"]? || "",
17 | }
18 | end
19 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | database: postgresql://localhost/crystal_ann
3 |
4 | development:
5 | <<: *default
6 |
7 | staging:
8 | <<: *default
9 |
10 | production:
11 | <<: *default
12 |
13 | test:
14 | database: postgresql://localhost/crystal_ann_test
15 |
--------------------------------------------------------------------------------
/config/deploy/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/config/deploy/.gitkeep
--------------------------------------------------------------------------------
/config/initializers/database.cr:
--------------------------------------------------------------------------------
1 | require "yaml"
2 |
3 | struct DBSettings
4 | getter database_url
5 |
6 | def initialize(@database_url : String)
7 | end
8 |
9 | def self.load(env = AMBER_ENV)
10 | config = YAML.parse(File.read "config/database.yml")[env]
11 | DBSettings.new config["database"].as_s
12 | end
13 | end
14 |
15 | ENV["DATABASE_URL"] ||= DBSettings.load.database_url
16 |
--------------------------------------------------------------------------------
/config/initializers/granite.cr:
--------------------------------------------------------------------------------
1 | Granite::ORM.settings.logger = Amber.settings.logger
2 |
--------------------------------------------------------------------------------
/config/initializers/hashids.cr:
--------------------------------------------------------------------------------
1 | require "hashids"
2 |
3 | HASHIDS = Hashids.new(salt: ENV["HASHIDS_SALT"]? || "crystal-ann-dev", min_length: 5)
4 |
--------------------------------------------------------------------------------
/config/initializers/multiauth.cr:
--------------------------------------------------------------------------------
1 | require "multi_auth"
2 |
3 | MultiAuth.config("github", ENV.fetch("GITHUB_ID", ""), ENV.fetch("GITHUB_SECRET", ""))
4 | MultiAuth.config("twitter", ENV.fetch("TWITTER_OAUTH_CONSUMER_KEY", ""), ENV.fetch("TWITTER_OAUTH_CONSUMER_SECRET", ""))
5 |
--------------------------------------------------------------------------------
/config/initializers/sidekiq.cr:
--------------------------------------------------------------------------------
1 | require "sidekiq"
2 |
3 | Sidekiq::Client.default_context = Sidekiq::Client::Context.new
4 |
--------------------------------------------------------------------------------
/config/initializers/site.cr:
--------------------------------------------------------------------------------
1 | require "yaml"
2 |
3 | struct SiteSettings
4 | getter name, description, url, force_ssl
5 |
6 | def initialize(@name : String, @description : String, @url : String, @force_ssl : Bool)
7 | end
8 |
9 | def self.load(env = AMBER_ENV)
10 | config = YAML.parse(File.read "config/site.yml")[env]
11 |
12 | SiteSettings.new config["name"].as_s,
13 | config["description"].as_s,
14 | config["url"].as_s,
15 | config["force_ssl"] == "true"
16 | end
17 | end
18 |
19 | SITE = SiteSettings.load
20 |
--------------------------------------------------------------------------------
/config/routes.cr:
--------------------------------------------------------------------------------
1 | Amber::Server.configure do |app|
2 | pipeline :web do
3 | # Plug is the method to use connect a pipe (middleware)
4 | # A plug accepts an instance of HTTP::Handler
5 | plug Amber::Pipe::Error.new
6 | plug Amber::Pipe::Logger.new
7 | # plug Amber::Pipe::Flash.new
8 | plug Amber::Pipe::Session.new
9 | plug Amber::Pipe::CSRF.new
10 | plug Amber::Pipe::PoweredByAmber.new
11 | end
12 |
13 | # All static content will run these transformations
14 | pipeline :static do
15 | plug Amber::Pipe::Error.new
16 | plug Amber::Pipe::Static.new("./public")
17 | plug HTTP::CompressHandler.new
18 | end
19 |
20 | routes :static do
21 | # Each route is defined as follow
22 | # verb resource : String, controller : Symbol, action : Symbol
23 | get "/*", Amber::Controller::Static, :index
24 | get "/about", StaticController, :about
25 | end
26 |
27 | routes :web do
28 | resources "/announcements", AnnouncementController
29 | get "/announcements/random", AnnouncementController, :random
30 | get "/=:hashid", AnnouncementController, :expand
31 | get "/rss", RSSController, :show
32 |
33 | get "/oauth/new", OAuthController, :new
34 | get "/oauth/:provider", OAuthController, :authenticate
35 | delete "/sessions", SessionController, :destroy
36 |
37 | get "/me", UserController, :me
38 | get "/users/:login", UserController, :show
39 | put "/users/remove_handle", UserController, :remove_handle
40 |
41 | get "/", AnnouncementController, :index
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/config/site.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | name: Crystal [ANN]
3 | description: Announce new project, blog post, version update or any other Crystal work
4 | force_ssl: false
5 |
6 | test:
7 | <<: *default
8 | url: https://testing.com
9 |
10 | development:
11 | <<: *default
12 | url: http://localhost:3008
13 |
14 | staging:
15 | <<: *default
16 | url: https://crystal-ann-staging.herokuapp.com
17 | force_ssl: true
18 |
19 | production:
20 | <<: *default
21 | url: https://crystal-ann.com
22 | force_ssl: true
23 |
--------------------------------------------------------------------------------
/db/data/announcements.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/db/data/announcements.json.gz
--------------------------------------------------------------------------------
/db/data/announcements.sql.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/db/data/announcements.sql.gz
--------------------------------------------------------------------------------
/db/migrate.cr:
--------------------------------------------------------------------------------
1 | require "../config/application"
2 | require "micrate"
3 | require "pg"
4 |
5 | if ENV["MICRATE_RUN_UP"]?
6 | Micrate::DB.connection_url = ENV["DATABASE_URL"]
7 | Micrate::Cli.setup_logger
8 | Micrate::Cli.run_up
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrations/20170701230806_create_announcement.sql:
--------------------------------------------------------------------------------
1 | -- +micrate Up
2 | CREATE TABLE announcements (
3 | id BIGSERIAL PRIMARY KEY,
4 | title VARCHAR,
5 | description TEXT,
6 | type INTEGER,
7 | created_at TIMESTAMP,
8 | updated_at TIMESTAMP
9 | );
10 |
11 | -- +micrate Down
12 | DROP TABLE IF EXISTS announcements;
13 |
--------------------------------------------------------------------------------
/db/migrations/20170710175120_create_user.sql:
--------------------------------------------------------------------------------
1 | -- +micrate Up
2 | CREATE TABLE users (
3 | id BIGSERIAL PRIMARY KEY,
4 | name VARCHAR,
5 | login VARCHAR,
6 | uid VARCHAR,
7 | provider VARCHAR,
8 | created_at TIMESTAMP,
9 | updated_at TIMESTAMP
10 | );
11 | CREATE UNIQUE INDEX uid_provider_idx ON users (uid, provider);
12 |
13 | ALTER TABLE announcements ADD COLUMN user_id BIGSERIAL REFERENCES users (id);
14 |
15 | -- +micrate Down
16 |
17 | ALTER TABLE announcements DROP COLUMN user_id CASCADE;
18 |
19 | DROP TABLE IF EXISTS users;
20 |
--------------------------------------------------------------------------------
/db/migrations/20170712100522_add_roles_to_users.sql:
--------------------------------------------------------------------------------
1 | -- +micrate Up
2 | -- SQL in section 'Up' is executed when this migration is applied
3 |
4 | ALTER TABLE users ADD COLUMN role VARCHAR DEFAULT 'user';
5 |
6 | UPDATE users SET role='admin' WHERE login='veelenga';
7 |
8 | -- +micrate Down
9 | -- SQL section 'Down' is executed when this migration is rolled back
10 |
11 | ALTER TABLE users DROP COLUMN role;
12 |
--------------------------------------------------------------------------------
/db/migrations/20170919173941_change_timestamp_columns.sql:
--------------------------------------------------------------------------------
1 | -- +micrate Up
2 | ALTER TABLE announcements
3 | ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE
4 | USING created_at AT TIME ZONE 'UTC';
5 |
6 | ALTER TABLE announcements
7 | ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE
8 | USING updated_at AT TIME ZONE 'UTC';
9 |
10 | ALTER TABLE users
11 | ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE
12 | USING created_at AT TIME ZONE 'UTC';
13 |
14 | ALTER TABLE users
15 | ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE
16 | USING updated_at AT TIME ZONE 'UTC';
17 |
18 | -- +micrate Down
19 | ALTER TABLE announcements
20 | ALTER COLUMN created_at TYPE TIMESTAMP
21 | USING created_at AT TIME ZONE 'UTC';
22 |
23 | ALTER TABLE announcements
24 | ALTER COLUMN updated_at TYPE TIMESTAMP
25 | USING updated_at AT TIME ZONE 'UTC';
26 |
27 | ALTER TABLE users
28 | ALTER COLUMN created_at TYPE TIMESTAMP
29 | USING created_at AT TIME ZONE 'UTC';
30 |
31 | ALTER TABLE users
32 | ALTER COLUMN updated_at TYPE TIMESTAMP
33 | USING updated_at AT TIME ZONE 'UTC';
34 |
--------------------------------------------------------------------------------
/db/migrations/20171002221739_add_handle_to_users.sql:
--------------------------------------------------------------------------------
1 | -- +micrate Up
2 | ALTER TABLE users ADD COLUMN handle VARCHAR;
3 |
4 | -- +micrate Down
5 | ALTER TABLE users DROP COLUMN handle;
6 |
--------------------------------------------------------------------------------
/db/seeds.cr:
--------------------------------------------------------------------------------
1 | require "../config/application"
2 | require "../src/models/**"
3 | require "./seeds/*"
4 |
5 | Announcement.clear
6 | User.clear
7 |
8 | Seeds::Users.create_records
9 | Seeds::Announcements.create_records
10 |
--------------------------------------------------------------------------------
/db/seeds/announcements.cr:
--------------------------------------------------------------------------------
1 | module Seeds::Announcements
2 | extend self
3 |
4 | def user_id
5 | user = User.find_by(:login, "veelenga") ||
6 | Seeds::Users.user(login: "veelenga", name: "V. Elenhaupt", provider: "github", uid: "1111")
7 | user.id.not_nil!.to_i
8 | end
9 |
10 | def announcement(typename, **params)
11 | type = Announcement::TYPES
12 | .find { |k, v| v == typename }
13 | .try(&.first) || 0
14 |
15 | attributes = {:type => type.to_s, :user_id => user_id.to_s} of String | Symbol => String | JSON::Type
16 | Announcement.new(attributes.merge!(params.to_h)).tap do |a|
17 | a.created_at = rand(15).days.ago - rand(60).minutes
18 | a.save
19 | end
20 | end
21 |
22 | def create_records
23 | announcement "Project Update",
24 | title: "Linear algebra library based on LAPACK",
25 | description: <<-DE
26 | https://github.com/konovod/linalg
27 |
28 | Linear algebra library in Crystal, uses LAPACKE:
29 |
30 | * direct access to LAPACK methods
31 | * convenient Matrix(T) class, supports T=Float32, Float64 and Complex.
32 | * high-level interface similar to scipy.linalg or MATLAB.
33 |
34 | Killing SciPy, one module at a time.
35 | DE
36 |
37 | announcement "Blog Post",
38 | title: "Methods tap and itself in Crystal",
39 | description: <<-DE
40 | http://veelenga.com/tap-and-itself-methods-in-crystal/
41 |
42 | Method [Object#tap](https://crystal-lang.org/api/0.23.0/Object.html#tap%28%26block%29-instance-method) in Crystal yields to the block and then returns self.
43 | [Object#itself](https://crystal-lang.org/api/0.23.0/Object.html#itself-instance-method) just returns self.
44 | Why do we need those methods? Let’s look at few examples.
45 | DE
46 |
47 | announcement "Blog Post",
48 | title: "Observer design pattern in Crystal language",
49 | description: <<-DE
50 | http://veelenga.com/observer-design-pattern-in-crystal-language/
51 |
52 | Being a developer you probably have heart about Observer design pattern.
53 | Perhaps, you have even used it in your complex system with subscribers and notifications.
54 | This article is about how to implement Observer pattern in Crystal language and what are the common features of this language we can use there to tune it.
55 |
56 | ```crystal
57 | # Sample
58 | fighter = Fighter.new("Scorpion")
59 |
60 | fighter.add_observer(Stats.new)
61 | fighter.add_observer(DieAction.new)
62 |
63 | fighter.damage(10)
64 | # Updating stats: Scorpion's health is 90
65 |
66 | fighter.damage(30)
67 | # Updating stats: Scorpion's health is 60
68 |
69 | fighter.damage(75)
70 | # Updating stats: Scorpion's health is 0
71 | # Scorpion is dead. Fight is over!
72 | ```
73 | DE
74 |
75 | announcement "Blog Post",
76 | title: "Make your own Shard in Crystal language",
77 | description: <<-DE
78 | http://veelenga.com/make-your-own-shard-in-crystal-language/
79 |
80 | An easy to use tutorial that describes how to create a new shard in Crystal language.
81 |
82 | Here you will find how to add an executable, project dependencies, write tests and document your code.
83 | DE
84 |
85 | announcement "Meetup",
86 | title: "Crystal Code Camp 2017",
87 | description: <<-DE
88 | Crystal Code Camp 2017 is taking place in San Francisco. See you there ;)
89 |
90 | Checkout our home page for details: https://codecamp.crystal-lang.org/
91 |
92 | Learn, engage and share.
93 | DE
94 |
95 | announcement "Project Update",
96 | title: "Crystal bindings to Lua and a wrapper around it",
97 | description: <<-DE
98 | 
99 |
100 | Crystal bindings to Lua and a wrapper around it: [https://github.com/veelenga/lua.cr](https://github.com/veelenga/lua.cr)
101 |
102 | ### Running chunk of Lua code:
103 |
104 | ```crystal
105 | Lua.run %q{
106 | local hello_message = table.concat({ 'Hello', 'from', 'Lua!' }, ' ')
107 | print(hello_message)
108 | } # => prints 'Hello from Lua!'
109 | ```
110 |
111 | ### Running Lua file:
112 |
113 | ```crystal
114 | p Lua.run File.new("./examples/sample.lua") # => 42.0
115 | ```
116 |
117 | ### Evaluate a function and pass arguments in:
118 |
119 | ```crystal
120 | lua = Lua.load
121 | sum = lua.run %q{
122 | function sum(x, y)
123 | return x + y
124 | end
125 |
126 | return sum
127 | }
128 | p sum.as(Lua::Function).call(3.2, 1) # => 4.2
129 | lua.close
130 | ```
131 |
132 | More features coming soon. Try it, that's fun :)
133 | DE
134 |
135 | announcement "Project Update",
136 | title: " Message Queue written in Crystal, very similar to NSQ",
137 | description: <<-DE
138 | https://github.com/crystalmq/crystalmq
139 |
140 | To run this project you will need three components.
141 |
142 | * A Message Router, running with either debug set to true or false
143 |
144 | `crystal router.cr false`
145 |
146 | * A Message Consumer, with a topic and channel set
147 |
148 | `crystal tools/consumer.cr my_topic my_channel`
149 |
150 | * A Message Producer, with a topic and message
151 |
152 | `crystal tools/producer.cr my_topic "Hello World"`
153 |
154 | * If you would like to run the benchmarks, feel free to use tools/meth
155 |
156 | ```
157 | ./meth --consumer # Will benchmark received messags (used in conjuction with producer)
158 | ./meth --producer # Will benchmark sent messages (used in conjunction with consumer)
159 | ./meth --latency # Will benchmark request latency
160 | ```
161 | DE
162 |
163 | announcement "Project Update",
164 | title: "FANN (Fast Artifical Neural Network) binding in Crystal",
165 | description: <<-DE
166 | [Crystal bindings for the FANN C lib](https://github.com/bararchy/crystal-fann)
167 |
168 | ```crystal
169 | require "crystal-fann"
170 | ann = Crystal::Fann::Network.new 2, [2], 1
171 | 500.times do
172 | ann.train_single [1.0_f32, 0.1_f32], [0.5_f32]
173 | end
174 |
175 | result = ann.run [1.0_f32, 0.1_f32]
176 |
177 | ann.close
178 | ```
179 | DE
180 |
181 | announcement "Other",
182 | title: "CrystalShards has reached 2000 shards!",
183 | description: <<-DE
184 | 
185 |
186 | Search your project at [crystalshards.xyz](http://crystalshards.xyz/)
187 | DE
188 |
189 | announcement "Blog Post",
190 | title: "Crystal and Kemal for dynamic website",
191 | description: <<-DE
192 | 
193 |
194 | [Second part](https://medium.com/@codelessfuture/crystal-and-kemal-for-dynamic-website-9b853481c88) of Crystal and Kemal pair for creating a dynamic website. Here you will find how to work with static files and layouts.
195 |
196 | In the [first article](https://hackernoon.com/starting-a-project-with-crystal-and-kemal-90e2647e6c3b) we prepare the development environment and made our hands dirty of the first lines of code.
197 | DE
198 |
199 | announcement "Project Update",
200 | title: "Crystal 0.23.0 has been released",
201 | description: <<-DE
202 | 
203 |
204 | [This release](https://github.com/crystal-lang/crystal/releases/tag/0.23.0) has been built with **LLVM 3.8**.
205 |
206 | As discussed in crystal-lang/omnibus-crystal#17, that means dropping support for Debian 7, and CentOS. Users on those platforms can still build from source using **LLVM 3.5**, but that's not recommended since it's buggy (see #4104).
207 |
208 | * **(breaking-change)** `Logger#formatter` takes a `Severity` instead of a `String` (See #4355, #4369, thanks @Sija)
209 | * **(breaking-change)** Removed `IO.select` (See #4392, thanks @RX14)
210 | * Added `Crystal::System::Random` namespace (See #4450, thanks @ysbaddaden)
211 | * Added `Path#resolve?` macro method (See #4370, #4408, thanks @RX14)
212 | * Added range methods to `BitArray` (See #4397, #3968, thanks @RX14)
213 | * Added some well-known HTTP Status messages (See #4419, thanks @akzhan)
214 | * Added compiler progress indicator (See #4182, thanks @RX14)
215 | * Added `System.cpu_cores` (See #4449, #4226, thanks @miketheman)
216 | * Added `separator` and `quote_char` to `CSV#each_row` (See #4448, thanks @timsu)
217 | * Added `map_with_index!` to `Pointer`, `Array` and `StaticArray` (See #4456, #3356, #3354, thanks @Nephos)
218 | * Added `headers` parameter to `HTTP::WebSocket` constructors (See #4227, #4222, thanks @adamtrilling)
219 | * `HTTP::StaticFileHandler` can disable directory listing (See #4403, #4398, thanks @joaodiogocosta)
220 | * `bin/crystal` now uses `/bin/sh` instead of `/bin/bash` (See #3809, #4410, thanks @TheLonelyGhost)
221 | * `crystal init` generates a `.editorconfig` file (See #4422, #297, thanks @akzhan)
222 | * `man` page for `crystal` command (See #2989, #1291, thanks @dread-uo)
223 | * Re-raising an exception doesn't overwrite its callstack (See #4487, #4482, thanks @akzhan)
224 | * MD5 and SHA1 documentation clearly states they are not cryptographically secure anymore (See #4426, thanks @RX14)
225 | * Fixed Crystal not reusing .o files across builds (See #4336)
226 | * Fixed `SomeClass.class.is_a?(SomeConst)` causing an "already had enclosing call" exception (See #4364, #4390, thanks @rockwyc992)
227 | * Fixed `HTTP::Params.parse` query string with two `=` gave wrong result (See #4388, #4389, thanks @akiicat)
228 | * Fixed `Class.class.is_a?(Class.class.class.class.class)` (See #4375, #4374, thanks @rockwyc992)
229 | * Fixed select hanging when sending before receive (See #3862, #3899, thanks @kostya)
230 | * Fixed "Unknown key in access token json: id_token" error in OAuth2 client (See #4437)
231 | * Fixed macro lookup conflicting with method lookup when including on top level (See #236)
232 | * Fixed Vagrant images (see #4510, #4508, thanks @Val)
233 | DE
234 |
235 | announcement "Other",
236 | title: "How to implement to_json in Crystal lang",
237 | description: <<-DE
238 | This is an example of how to implement a generic to_json method in Crystal lang:
239 |
240 | * [https://snippets.aktagon.com/snippets/799-how-to-implement-to-json-in-crystal-lang](https://snippets.aktagon.com/snippets/799-how-to-implement-to-json-in-crystal-lang)
241 | DE
242 | end
243 | end
244 |
--------------------------------------------------------------------------------
/db/seeds/users.cr:
--------------------------------------------------------------------------------
1 | module Seeds::Users
2 | extend self
3 |
4 | def user(**params)
5 | User.new(params.to_h).tap &.save
6 | end
7 |
8 | def create_records
9 | user login: "veelenga",
10 | name: "V. Elenhaupt",
11 | provider: "github",
12 | uid: "111111",
13 | role: "admin"
14 |
15 | user login: "ann",
16 | name: "Ann Doe",
17 | provider: "github",
18 | uid: "22222"
19 |
20 | user login: "joe",
21 | name: "Joe Doe",
22 | provider: "github",
23 | uid: "33333"
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | app:
5 | build:
6 | context: .
7 | environment:
8 | - AMBER_ENV=production
9 | - AMBER_SESSION_SECRET=
10 |
11 | - DATABASE_URL=postgres://postgres:postgres@db:5432/crystal_ann
12 | - MICRATE_RUN_UP=true
13 |
14 | - GITHUB_ID=
15 | - GITHUB_SECRET=
16 |
17 | - TWITTER_CONSUMER_KEY=
18 | - TWITTER_CONSUMER_SECRET=
19 | - TWITTER_ACCESS_TOKEN=
20 | - TWITTER_ACCESS_TOKEN_SECRET=
21 | - TWITTER_OAUTH_CONSUMER_KEY=
22 | - TWITTER_OAUTH_CONSUMER_SECRET=
23 |
24 | depends_on:
25 | db:
26 | condition: service_healthy
27 | ports:
28 | - 3000:3008
29 |
30 | db:
31 | image: postgres:13.8-alpine
32 | environment:
33 | - POSTGRES_USER=postgres
34 | - POSTGRES_PASSWORD=postgres
35 | - POSTGRES_DB=crystal_ann
36 | volumes:
37 | - crystal_ann-db:/var/lib/postgres/data
38 | healthcheck:
39 | test: ["CMD-SHELL", "pg_isready -U postgres"]
40 | interval: 10s
41 | timeout: 10s
42 | start_period: 10s
43 |
44 | volumes:
45 | crystal_ann-db:
46 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crystal-ann",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "crystal-ann",
3 | "version": "1.0.0",
4 | "repository": "https://github.com/crystal-community/crystal-ann",
5 | "author": "V. Elenhaupt ",
6 | "license": "MIT",
7 | "private": true,
8 | "scripts": {
9 | "watch": "echo"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/public/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/public/favicon.ico
--------------------------------------------------------------------------------
/public/javascripts/highlight.pack.js:
--------------------------------------------------------------------------------
1 | /*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */
2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""+t(e)+">"}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?" ":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/ /g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("makefile",function(e){var i={cN:"variable",v:[{b:"\\$\\("+e.UIR+"\\)",c:[e.BE]},{b:/\$[@%\^\+\*]/}]},r={cN:"string",b:/"/,e:/"/,c:[e.BE,i]},a={cN:"variable",b:/\$\([\w-]+\s/,e:/\)/,k:{built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"},c:[i]},n={b:"^"+e.UIR+"\\s*[:+?]?=",i:"\\n",rB:!0,c:[{b:"^"+e.UIR,e:"[:+?]?=",eE:!0}]},t={cN:"meta",b:/^\.PHONY:/,e:/$/,k:{"meta-keyword":".PHONY"},l:/[\.\w]+/},l={cN:"section",b:/^[^\s]+:/,e:/$/,c:[i]};return{aliases:["mk","mak"],k:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath",l:/[\w-]+/,c:[e.HCM,i,r,a,n,t,l]}});hljs.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},s={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),s,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=o,s.c=o,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:o}});hljs.registerLanguage("php",function(e){var c={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},i={cN:"meta",b:/<\?(php)?|\?>/},t={cN:"string",c:[e.BE,i],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php3","php4","php5","php6"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[i]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},i,{cN:"keyword",b:/\$this\b/},c,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",c,e.CBCM,t,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},t,a]}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b:/,e:/(\/\w+|\w+\/)>/,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},i=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{sL:"javascript",eB:!0,eE:!0,v:[{b:"```",e:"```"},{b:"`",e:"`"}]}];r.c=i;var s=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(i)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:i.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[s,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[s]},s]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<(-?)\w+$/,e:/^\s*\w+$/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+"|unless)\\s*",k:"unless",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l="[>?]>",o="[\\w#]+\\(\\w+\\):\\d+:\\d+>",u="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",w=[{b:/^\s*=>/,starts:{e:"$",c:d}},{cN:"meta",b:"^("+l+"|"+o+"|"+u+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:s.concat(w).concat(d)}});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}});hljs.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("ini",function(e){var b={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},b,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}});hljs.registerLanguage("shell",function(s){return{aliases:["console"],c:[{cN:"meta",b:"^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]",starts:{e:"$",sL:"bash"}}]}});hljs.registerLanguage("crystal",function(e){function b(e,b){var r=[{b:e,e:b}];return r[0].c=r,r}var r="(_[uif](8|16|32|64))?",c="[a-zA-Z_]\\w*[!?=]?",i="!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\][=?]?",s={keyword:"abstract alias as as? asm begin break case class def do else elsif end ensure enum extend for fun if include instance_sizeof is_a? lib macro module next nil? of out pointerof private protected rescue responds_to? return require select self sizeof struct super then type typeof union uninitialized unless until when while with yield __DIR__ __END_LINE__ __FILE__ __LINE__",literal:"false nil true"},t={cN:"subst",b:"#{",e:"}",k:s},a={cN:"template-variable",v:[{b:"\\{\\{",e:"\\}\\}"},{b:"\\{%",e:"%\\}"}],k:s},l={cN:"string",c:[e.BE,t],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%w?\\(",e:"\\)",c:b("\\(","\\)")},{b:"%w?\\[",e:"\\]",c:b("\\[","\\]")},{b:"%w?{",e:"}",c:b("{","}")},{b:"%w?<",e:">",c:b("<",">")},{b:"%w?/",e:"/"},{b:"%w?%",e:"%"},{b:"%w?-",e:"-"},{b:"%w?\\|",e:"\\|"},{b:/<<-\w+$/,e:/^\s*\w+$/}],r:0},u={cN:"string",v:[{b:"%q\\(",e:"\\)",c:b("\\(","\\)")},{b:"%q\\[",e:"\\]",c:b("\\[","\\]")},{b:"%q{",e:"}",c:b("{","}")},{b:"%q<",e:">",c:b("<",">")},{b:"%q/",e:"/"},{b:"%q%",e:"%"},{b:"%q-",e:"-"},{b:"%q\\|",e:"\\|"},{b:/<<-'\w+'$/,e:/^\s*\w+$/}],r:0},_={b:"("+i+")\\s*",c:[{cN:"regexp",c:[e.BE,t],v:[{b:"//[a-z]*",r:0},{b:"/",e:"/[a-z]*"},{b:"%r\\(",e:"\\)",c:b("\\(","\\)")},{b:"%r\\[",e:"\\]",c:b("\\[","\\]")},{b:"%r{",e:"}",c:b("{","}")},{b:"%r<",e:">",c:b("<",">")},{b:"%r/",e:"/"},{b:"%r%",e:"%"},{b:"%r-",e:"-"},{b:"%r\\|",e:"\\|"}]}],r:0},o={cN:"regexp",c:[e.BE,t],v:[{b:"%r\\(",e:"\\)",c:b("\\(","\\)")},{b:"%r\\[",e:"\\]",c:b("\\[","\\]")},{b:"%r{",e:"}",c:b("{","}")},{b:"%r<",e:">",c:b("<",">")},{b:"%r/",e:"/"},{b:"%r%",e:"%"},{b:"%r-",e:"-"},{b:"%r\\|",e:"\\|"}],r:0},w={cN:"meta",b:"@\\[",e:"\\]",c:[e.inherit(e.QSM,{cN:"meta-string"})]},f=[a,l,u,_,o,w,e.HCM,{cN:"class",bK:"class module struct",e:"$|;",i:/=/,c:[e.HCM,e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<"}]},{cN:"class",bK:"lib enum union",e:"$|;",i:/=/,c:[e.HCM,e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"})],r:10},{cN:"function",bK:"def",e:/\B\b/,c:[e.inherit(e.TM,{b:n,endsParent:!0})]},{cN:"function",bK:"fun macro",e:/\B\b/,c:[e.inherit(e.TM,{b:n,endsParent:!0})],r:5},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":",c:[l,{b:n}],r:0},{cN:"number",v:[{b:"\\b0b([01_]*[01])"+r},{b:"\\b0o([0-7_]*[0-7])"+r},{b:"\\b0x([A-Fa-f0-9_]*[A-Fa-f0-9])"+r},{b:"\\b(([0-9][0-9_]*[0-9]|[0-9])(\\.[0-9_]*[0-9])?([eE][+-]?[0-9_]*[0-9])?)"+r}],r:0}];return t.c=f,a.c=f.slice(1),{aliases:["cr"],l:c,k:s,c:f}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/,r:0,c:[{cN:"attr",b:e,r:0},{b:/=\s*/,r:0,c:[{cN:"string",endsParent:!0,v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s"'=<>`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"?",e:"/?>",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("cs",function(e){var i={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",literal:"null false true"},t={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},r=e.inherit(t,{i:/\n/}),a={cN:"subst",b:"{",e:"}",k:i},c=e.inherit(a,{i:/\n/}),n={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,c]},s={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},a]},o=e.inherit(s,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},c]});a.c=[s,n,t,e.ASM,e.QSM,e.CNM,e.CBCM],c.c=[o,n,r,e.ASM,e.QSM,e.CNM,e.inherit(e.CBCM,{i:/\n/})];var l={v:[s,n,t,e.ASM,e.QSM]},b=e.IR+"(<"+e.IR+"(\\s*,\\s*"+e.IR+")*>)?(\\[\\])?";return{aliases:["csharp"],k:i,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:""},{b:"?",e:">"}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},l,e.CNM,{bK:"class interface",e:/[{;=]/,i:/[^\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{cN:"meta",b:"^\\s*\\[",eB:!0,e:"\\]",eE:!0,c:[{cN:"meta-string",b:/"/,e:/"/}]},{bK:"new return throw await else",r:0},{cN:"function",b:"("+b+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:i,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:i,r:0,c:[l,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}});hljs.registerLanguage("apache",function(e){var r={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:"?",e:">"},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",r]},r,e.QSM]}}],i:/\S/}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("python",function(e){var r={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},b={cN:"meta",b:/^(>>>|\.\.\.) /},c={cN:"subst",b:/\{/,e:/\}/,k:r,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[b],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[b,c]},{b:/(fr|rf|f)"""/,e:/"""/,c:[b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[c]},{b:/(fr|rf|f)"/,e:/"/,c:[c]},e.ASM,e.QSM]},s={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},i={cN:"params",b:/\(/,e:/\)/,c:["self",b,s,a]};return c.c=[a,s,b],{aliases:["py","gyp"],k:r,i:/(<\/|->|\?)|=>/,c:[b,s,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("cpp",function(t){var e={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U)?L?"',e:'"',i:"\\n",c:[t.BE]},{b:'(u8?|U)?R"',e:'"',c:[t.BE]},{b:"'\\\\?.",e:"'",i:"."}]},s={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},i={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},t.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:/<[^\n>]*>/,e:/$/,i:"\\n"},t.CLCM,t.CBCM]},a=t.IR+"\\s*\\(",c={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:["c","cc","h","c++","h++","hpp"],k:c,i:"",c:n.concat([i,{b:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",e:">",k:c,c:["self",e]},{b:t.IR+"::",k:c},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:c,c:n.concat([{b:/\(/,e:/\)/,k:c,c:n.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+t.IR+"[\\*&\\s]+)+"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\w\s\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e]},t.CLCM,t.CBCM,i]},{cN:"class",bK:"class struct",e:/[{;:]/,c:[{b:/,e:/>/,c:["self"]},t.TM]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage("objectivec",function(e){var t={cN:"built_in",b:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},_={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},i=/[a-zA-Z@][a-zA-Z0-9_]*/,n="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:_,l:i,i:"",c:[t,e.CLCM,e.CBCM,e.CNM,e.QSM,{cN:"string",v:[{b:'@"',e:'"',i:"\\n",c:[e.BE]},{b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"}]},{cN:"meta",b:"#",e:"$",c:[{cN:"meta-string",v:[{b:'"',e:'"'},{b:"<",e:">"}]}]},{cN:"class",b:"("+n.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:n,l:i,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}});hljs.registerLanguage("java",function(e){var a="[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",t=a+"(<"+a+"(\\s*,\\s*"+a+")*>)?",r="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",s="\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",c={cN:"number",b:s,r:0};return{aliases:["jsp"],k:r,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"("+t+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:r,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:r,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},c,{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("nginx",function(e){var r={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},b={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,r],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[r]},{cN:"regexp",c:[e.BE,r],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},r]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:b}],r:0}],i:"[^\\s\\}]"}});
--------------------------------------------------------------------------------
/public/javascripts/main.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("DOMContentLoaded", (event) => {
2 | toggleSecondary();
3 | signoutListener();
4 | runPrettify();
5 | runHighlighter();
6 | });
7 |
8 | function toggleSecondary() {
9 | var button = document.getElementsByClassName("secondary-toggle")[0];
10 | var secondary = document.getElementsByClassName("secondary")[0];
11 |
12 | if (secondary && button) {
13 | button.addEventListener("click", () =>
14 | secondary.classList.toggle("toggled-on"));
15 | }
16 | }
17 |
18 | function signoutListener() {
19 | var button = document.getElementById("sign-out-btn");
20 | if (button) {
21 | button.addEventListener("click", (e) => {
22 | e.preventDefault();
23 |
24 | var form = document.createElement("form");
25 | form.method = "POST";
26 | form.action = "/sessions";
27 |
28 | var method = document.createElement("input");
29 | method.name = "_method";
30 | method.value = "delete";
31 | form.appendChild(method);
32 |
33 | var csrf = document.createElement("input");
34 | csrf.type = "hidden";
35 | csrf.name = "_csrf";
36 | csrf.value = button.getAttribute("csrf-token");
37 | form.appendChild(csrf);
38 |
39 | document.body.appendChild(form);
40 | form.submit();
41 | });
42 | }
43 | }
44 |
45 | function runPrettify() {
46 | hljs.initHighlightingOnLoad();
47 | }
48 |
49 | function runHighlighter() {
50 | var search = document.getElementById("search-input");
51 | if (search) {
52 | var context = document.querySelectorAll(".entry-header, .entry-content");
53 | new Mark(context).mark(search.value);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/launcher-icon/launcher-icon-1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/public/launcher-icon/launcher-icon-1x.png
--------------------------------------------------------------------------------
/public/launcher-icon/launcher-icon-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/public/launcher-icon/launcher-icon-2x.png
--------------------------------------------------------------------------------
/public/launcher-icon/launcher-icon-3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/public/launcher-icon/launcher-icon-3x.png
--------------------------------------------------------------------------------
/public/launcher-icon/launcher-icon-4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/public/launcher-icon/launcher-icon-4x.png
--------------------------------------------------------------------------------
/public/launcher-icon/launcher-icon-5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/public/launcher-icon/launcher-icon-5x.png
--------------------------------------------------------------------------------
/public/launcher-icon/launcher-icon-6x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crystal-community/crystal-ann/f58bb9d97a24d1fc68374fc8143b8987451f3594/public/launcher-icon/launcher-icon-6x.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Crystal Announcements",
3 | "short_name": "Crystal ANN",
4 | "display": "standalone",
5 | "start_url": "/?utm_source=homescreen",
6 | "background_color": "#F1F1F1",
7 | "theme_color": "#FFFFFF",
8 | "orientation": "portrait",
9 | "description": "Announce new project, blog post, version update or any other Crystal work",
10 | "icons": [{
11 | "src": "/launcher-icon/launcher-icon-1x.png",
12 | "sizes": "48x48",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "/launcher-icon/launcher-icon-2x.png",
17 | "sizes": "72x72",
18 | "type": "image/png"
19 | }, {
20 | "src": "/launcher-icon/launcher-icon-3x.png",
21 | "sizes": "80x80",
22 | "type": "image/png"
23 | }, {
24 | "src": "/launcher-icon/launcher-icon-4x.png",
25 | "sizes": "96x96",
26 | "type": "image/png"
27 | }, {
28 | "src": "/launcher-icon/launcher-icon-5x.png",
29 | "sizes": "144x144",
30 | "type": "image/png"
31 | }, {
32 | "src": "/launcher-icon/launcher-icon-6x.png",
33 | "sizes": "192x192",
34 | "type": "image/png"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # http://www.robotstxt.org
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/stylesheets/monokai-sublime.min.css:
--------------------------------------------------------------------------------
1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#23241f}.hljs,.hljs-tag,.hljs-subst{color:#f8f8f2}.hljs-strong,.hljs-emphasis{color:#a8a8a2}.hljs-bullet,.hljs-quote,.hljs-number,.hljs-regexp,.hljs-literal,.hljs-link{color:#ae81ff}.hljs-code,.hljs-title,.hljs-section,.hljs-selector-class{color:#a6e22e}.hljs-strong{font-weight:bold}.hljs-emphasis{font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-name,.hljs-attr{color:#f92672}.hljs-symbol,.hljs-attribute{color:#66d9ef}.hljs-params,.hljs-class .hljs-title{color:#f8f8f2}.hljs-string,.hljs-type,.hljs-built_in,.hljs-builtin-name,.hljs-selector-id,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-addition,.hljs-variable,.hljs-template-variable{color:#e6db74}.hljs-comment,.hljs-deletion,.hljs-meta{color:#75715e}
--------------------------------------------------------------------------------
/shard.lock:
--------------------------------------------------------------------------------
1 | version: 1.0
2 | shards:
3 | amber:
4 | github: amberframework/amber
5 | version: 0.6.7
6 |
7 | ameba:
8 | github: veelenga/ameba
9 | version: 0.5.0
10 |
11 | autolink:
12 | github: crystal-community/autolink.cr
13 | version: 0.1.3
14 |
15 | baked_file_system:
16 | github: schovi/baked_file_system
17 | version: 0.9.5
18 |
19 | callback:
20 | github: mosop/callback
21 | version: 0.6.3
22 |
23 | cli:
24 | github: amberframework/cli
25 | version: 0.7.0
26 |
27 | db:
28 | github: crystal-lang/crystal-db
29 | version: 0.5.0
30 |
31 | granite_orm:
32 | github: amberframework/granite-orm
33 | version: 0.8.4
34 |
35 | hashids:
36 | github: splattael/hashids.cr
37 | version: 0.2.2
38 |
39 | kemal:
40 | github: kemalcr/kemal
41 | version: 0.22.0
42 |
43 | kemal-csrf:
44 | github: kemalcr/kemal-csrf
45 | version: 0.4.0
46 |
47 | kemal-session:
48 | github: kemalcr/kemal-session
49 | version: 0.9.0
50 |
51 | kilt:
52 | github: jeromegn/kilt
53 | version: 0.4.0
54 |
55 | micrate:
56 | github: juanedi/micrate
57 | version: 0.3.3
58 |
59 | multi_auth:
60 | github: msa7/multi_auth
61 | commit: 4b8a34be7b6ee65e1efe42f352465f6f27323092
62 |
63 | mysql:
64 | github: crystal-lang/crystal-mysql
65 | version: 0.4.0
66 |
67 | optarg:
68 | github: mosop/optarg
69 | version: 0.5.8
70 |
71 | pg:
72 | github: will/crystal-pg
73 | version: 0.14.1
74 |
75 | pool:
76 | github: ysbaddaden/pool
77 | version: 0.2.3
78 |
79 | radix:
80 | github: luislavena/radix
81 | version: 0.3.8
82 |
83 | redis:
84 | github: stefanwille/crystal-redis
85 | version: 1.9.0
86 |
87 | shell-table:
88 | github: jwaldrip/shell-table.cr
89 | version: 0.9.2
90 |
91 | sidekiq:
92 | github: mperham/sidekiq.cr
93 | commit: 26253ed561094e715f27f3d4e4c9679876d26aee
94 |
95 | slang:
96 | github: jeromegn/slang
97 | version: 1.7.1
98 |
99 | spec2:
100 | github: waterlink/spec2.cr
101 | version: 0.11.0
102 |
103 | spinner:
104 | github: askn/spinner
105 | version: 0.1.1
106 |
107 | sqlite3:
108 | github: crystal-lang/crystal-sqlite3
109 | version: 0.9.0
110 |
111 | string_inflection:
112 | github: mosop/string_inflection
113 | version: 0.2.1
114 |
115 | teeplate:
116 | github: amberframework/teeplate
117 | version: 0.5.0
118 |
119 | twitter-crystal:
120 | github: sferik/twitter-crystal
121 | version: 0.2.1
122 |
123 | webmock:
124 | github: manastech/webmock.cr
125 | commit: 7e7e7ff4d00afbb708909e1721e9f9bceddda726
126 |
127 |
--------------------------------------------------------------------------------
/shard.yml:
--------------------------------------------------------------------------------
1 | name: crystal-ann
2 | version:
3 |
4 | authors:
5 | - V. Elenhaupt
6 | - Hugo Abonizio
7 |
8 | crystal: 0.24.2
9 |
10 | license: MIT
11 |
12 | dependencies:
13 | granite_orm:
14 | github: amberframework/granite-orm
15 | version: 0.8.4
16 |
17 | multi_auth:
18 | github: msa7/multi_auth
19 | commit: 4b8a34be7b6ee65e1efe42f352465f6f27323092
20 |
21 | micrate:
22 | github: juanedi/micrate
23 | version: 0.3.3
24 |
25 | sidekiq:
26 | github: mperham/sidekiq.cr
27 | commit: 26253ed561094e715f27f3d4e4c9679876d26aee
28 |
29 | amber:
30 | github: amberframework/amber
31 | version: 0.6.7
32 |
33 | twitter-crystal:
34 | github: sferik/twitter-crystal
35 | version: 0.2.1
36 |
37 | hashids:
38 | github: splattael/hashids.cr
39 | version: 0.2.2
40 |
41 | autolink:
42 | github: crystal-community/autolink.cr
43 | version: 0.1.3
44 |
45 | development_dependencies:
46 | spec2:
47 | github: waterlink/spec2.cr
48 | version: 0.11.0
49 |
50 | webmock:
51 | github: manastech/webmock.cr
52 | commit: 7e7e7ff4d00afbb708909e1721e9f9bceddda726
53 |
54 | ameba:
55 | github: veelenga/ameba
56 | version: 0.5.0
57 |
--------------------------------------------------------------------------------
/spec/controllers/announcement_controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe AnnouncementController do
4 | let(:user) { user(login: "JohnDoe").tap &.save }
5 | let(:another_user) { user(login: "superman").tap &.save }
6 | let(:admin_user) { user(login: "superman", role: "admin").tap &.save }
7 |
8 | let(:title) { "Announcement Controller Sample Title" }
9 | let(:announcement) { announcement(user, title: title).tap &.save }
10 |
11 | before do
12 | Announcement.clear
13 | User.clear
14 | end
15 |
16 | describe "GET index" do
17 | before { announcement }
18 |
19 | it "renders all the announcements" do
20 | get "/announcements"
21 | expect(response.status_code).to eq 200
22 | expect(response.body.includes? title).to be_true
23 | end
24 |
25 | context "with query param" do
26 | it "can find announcements matching title or description" do
27 | a1 = announcement(user, title: "This is amazing announcement").tap &.save
28 | a2 = announcement(user, title: "This announcement is also amazing").tap &.save
29 | a3 = announcement(user, title: "This announcement is just cool").tap &.save
30 |
31 | get "/announcements", body: "query=amazing"
32 | expect(response.body.includes? a1.title.to_s).to be_true
33 | expect(response.body.includes? a2.title.to_s).to be_true
34 | expect(response.body.includes? a3.title.to_s).to be_false
35 | end
36 | end
37 |
38 | context "with type param" do
39 | it "can find announcements by type" do
40 | a1 = announcement(user, type: 0_i64).tap &.save
41 | a2 = announcement(user, type: 0_i64).tap &.save
42 | a3 = announcement(user, type: 1_i64).tap &.save
43 |
44 | get "/announcements", body: "type=#{Announcement::TYPES[0]}"
45 | expect(response.body.includes? a1.title.to_s).to be_true
46 | end
47 | end
48 |
49 | context "with user param" do
50 | it "can find announcements by user login" do
51 | user1 = user(login: "Superman").tap &.save
52 | user2 = user(login: "Batman").tap &.save
53 |
54 | a1 = announcement(user1, title: "Announcement1").tap &.save
55 | a2 = announcement(user1, title: "Announcement2").tap &.save
56 | a3 = announcement(user2, title: "Announcement3").tap &.save
57 |
58 | get "/announcements", body: "user=#{user1.login}"
59 |
60 | expect(response.body.includes? a1.title.to_s).to be_true
61 | expect(response.body.includes? a2.title.to_s).to be_true
62 | expect(response.body.includes? a3.title.to_s).to be_false
63 | end
64 | end
65 | end
66 |
67 | describe "GET show" do
68 | it "renders a single announcement" do
69 | get "/announcements/#{announcement.id}"
70 | expect(response.status_code).to eq 200
71 | expect(response.body.includes? title).to be_true
72 | end
73 | end
74 |
75 | describe "GET new" do
76 | it "render new template" do
77 | get "/announcements/new"
78 | expect(response.status_code).to eq 200
79 | expect(response.body.includes? "Make an Announcement").to be_true
80 | end
81 |
82 | context "rate limit reached" do
83 | before { login_as user; ENV["MAX_ANNS_PER_HOUR"] = "0" }
84 | after { ENV["MAX_ANNS_PER_HOUR"] = nil }
85 |
86 | it "shows rate limit message" do
87 | get "/announcements/new"
88 | expect(response.status_code).to eq 200
89 | expect(response.body.includes? "You can create up to").to be_true
90 | end
91 | end
92 | end
93 |
94 | describe "POST create" do
95 | context "when user signed-in" do
96 | before { login_as user }
97 |
98 | it "creates an announcement with valid params" do
99 | post "/announcements", body: "title=test-title&description=test-description&type=0"
100 | expect(Announcement.all.size).to eq 1
101 | end
102 |
103 | it "does not create an announcement with invalid params" do
104 | post "/announcements", body: "title=test-title"
105 | expect(Announcement.all.size).to eq 0
106 | end
107 |
108 | it "does not create an announcement if csrf token is invalid" do
109 | token = Base64.encode "invalid-token"
110 | post "/announcements", body: "title=test-title&description=test-description&type=0&_csrf=#{token}"
111 | expect(response.status_code).to eq 403
112 | expect(Announcement.all.size).to eq 0
113 | end
114 |
115 | context "and rate limit is reached" do
116 | before { ENV["MAX_ANNS_PER_HOUR"] = "0" }
117 | after { ENV["MAX_ANNS_PER_HOUR"] = nil }
118 |
119 | it "redirects to /" do
120 | post "/announcements", body: "title=test-title&description=test-description&type=0"
121 | expect(response.status_code).to eq 302
122 | expect(response).to redirect_to "/"
123 | expect(Announcement.all.size).to eq 0
124 | end
125 | end
126 | end
127 |
128 | context "when user not signed-in" do
129 | it "does not create an announcement with valid params" do
130 | post "/announcements", body: "title=test-title&description=test-description&type=0"
131 | expect(response.status_code).to eq 302
132 | expect(Announcement.all.size).to eq 0
133 | end
134 |
135 | it "redirects to /" do
136 | post "/announcements", body: "title=test-title&description=test-description&type=0"
137 | expect(response).to redirect_to "/announcements/new"
138 | end
139 | end
140 | end
141 |
142 | describe "GET edit" do
143 | context "when user not signed-in" do
144 | it "redirects to root_url" do
145 | get "/announcements/#{announcement.id}/edit"
146 | expect(response.status_code).to eq 302
147 | expect(response).to redirect_to "/"
148 | end
149 | end
150 |
151 | context "when user signed-in" do
152 | context "user can update this announcement" do
153 | before { login_as user }
154 |
155 | it "renders edit template" do
156 | get "/announcements/#{announcement.id}/edit"
157 | expect(response.status_code).to eq 200
158 | expect(response.body.includes? announcement.title.to_s).to be_true
159 | end
160 | end
161 |
162 | context "user can't update this announcement" do
163 | before { login_as another_user }
164 |
165 | it "redirects to root url" do
166 | get "/announcements/#{announcement.id}/edit"
167 | expect(response.status_code).to eq 302
168 | expect(response).to redirect_to "/"
169 | end
170 | end
171 |
172 | context "user is admin" do
173 | before { login_as admin_user }
174 |
175 | it "renders edit template" do
176 | get "/announcements/#{announcement.id}/edit"
177 | expect(response.status_code).to eq 200
178 | expect(response.body.includes? announcement.title.to_s).to be_true
179 | end
180 | end
181 | end
182 | end
183 |
184 | describe "PATCH update" do
185 | let(:valid_params) do
186 | {
187 | title: "my super cool title",
188 | description: "my super cool description",
189 | type: "1",
190 | }
191 | end
192 | let(:invalid_params) do
193 | {
194 | title: "-",
195 | description: "too short",
196 | type: "-1",
197 | }
198 | end
199 |
200 | context "when user not signed-in" do
201 | it "redirects to root url" do
202 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
203 | expect(response.status_code).to eq 302
204 | expect(response).to redirect_to "/"
205 | end
206 |
207 | it "does not update announcement" do
208 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
209 | expect(announcement.title).to eq title
210 | end
211 | end
212 |
213 | context "when user signed-in" do
214 | context "when user can update announcement" do
215 | before { login_as announcement.user.not_nil! }
216 |
217 | it "updates announcement if params are valid" do
218 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
219 | updated = Announcement.find(announcement.try &.id).not_nil!
220 | expect(updated.title).to eq valid_params[:title]
221 | expect(updated.description).to eq valid_params[:description]
222 | expect(updated.type).to eq valid_params[:type].to_i
223 | end
224 |
225 | it "redirects to announcement#show page if params are valid" do
226 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
227 | expect(response.status_code).to eq 302
228 | expect(response).to redirect_to "/announcements/#{announcement.id}"
229 | end
230 |
231 | it "does not update announcement if params are not valid" do
232 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(invalid_params)
233 | a = Announcement.find(announcement.try &.id).not_nil!
234 | expect(a.title).to eq announcement.title
235 | expect(a.description).to eq announcement.description
236 | expect(a.type).to eq announcement.type
237 | end
238 |
239 | it "renders announcement#edit page if params are not valid" do
240 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(invalid_params)
241 | expect(response.status_code).to eq 200
242 | end
243 |
244 | it "does not update announcement if csrf token is invalid" do
245 | token = Base64.encode "invalid-token"
246 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params) + "&_csrf=#{token}"
247 | expect(response.status_code).to eq 403
248 | a = Announcement.find(announcement.try &.id).not_nil!
249 | expect(a.title).to eq announcement.title
250 | expect(a.description).to eq announcement.description
251 | expect(a.type).to eq announcement.type
252 | end
253 | end
254 |
255 | context "when user cannot update announcement" do
256 | before { login_as another_user }
257 |
258 | it "does not update announcement" do
259 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
260 | a = Announcement.find(announcement.try &.id).not_nil!
261 | expect(a.title).to eq announcement.title
262 | expect(a.description).to eq announcement.description
263 | expect(a.type).to eq announcement.type
264 | end
265 |
266 | it "redirects to root url" do
267 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
268 | expect(response.status_code).to eq 302
269 | expect(response).to redirect_to "/"
270 | end
271 | end
272 |
273 | context "when user is admin" do
274 | before { login_as admin_user }
275 |
276 | it "updates announcement if params are valid" do
277 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
278 | updated = Announcement.find(announcement.try &.id).not_nil!
279 | expect(updated.title).to eq valid_params[:title]
280 | expect(updated.description).to eq valid_params[:description]
281 | expect(updated.type).to eq valid_params[:type].to_i
282 | end
283 |
284 | it "redirects to announcement#show page if params are valid" do
285 | patch "/announcements/#{announcement.id}", body: HTTP::Params.encode(valid_params)
286 | expect(response.status_code).to eq 302
287 | expect(response).to redirect_to "/announcements/#{announcement.id}"
288 | end
289 | end
290 | end
291 | end
292 |
293 | describe "DELETE destroy" do
294 | context "when user not signed-in" do
295 | it "redirects to root url" do
296 | delete "/announcements/#{announcement.id}"
297 | expect(response.status_code).to eq 302
298 | expect(response).to redirect_to "/"
299 | end
300 |
301 | it "does not delete announcement" do
302 | delete "/announcements/#{announcement.id}"
303 | expect(Announcement.find(announcement.try &.id)).not_to be_nil
304 | end
305 | end
306 |
307 | context "when user signed-in" do
308 | context "when user can update announcement" do
309 | before { login_as announcement.user.not_nil! }
310 |
311 | it "deletes announcement" do
312 | delete "/announcements/#{announcement.id}"
313 | expect(Announcement.find announcement.id).to be_nil
314 | end
315 |
316 | it "redirects to root url" do
317 | delete "/announcements/#{announcement.id}"
318 | expect(response.status_code).to eq 302
319 | expect(response).to redirect_to "/"
320 | end
321 |
322 | it "does not delete announcement if csrf token is invalid" do
323 | token = Base64.encode "invalid-token"
324 | delete "/announcements/#{announcement.id}", body: "_csrf=#{token}"
325 | expect(response.status_code).to eq 403
326 | expect(Announcement.find announcement.id).not_to be_nil
327 | end
328 | end
329 |
330 | context "when user cannot update announcement" do
331 | before { login_as another_user }
332 |
333 | it "does not delete announcement" do
334 | delete "/announcements/#{announcement.id}"
335 | expect(Announcement.find announcement.id).not_to be_nil
336 | end
337 |
338 | it "redirects to root url" do
339 | delete "/announcements/#{announcement.id}"
340 | expect(response.status_code).to eq 302
341 | expect(response).to redirect_to "/"
342 | end
343 | end
344 |
345 | context "when user is admin" do
346 | before { login_as admin_user }
347 |
348 | it "deletes announcement" do
349 | delete "/announcements/#{announcement.id}"
350 | expect(Announcement.find announcement.id).to be_nil
351 | end
352 |
353 | it "redirects to root url" do
354 | delete "/announcements/#{announcement.id}"
355 | expect(response.status_code).to eq 302
356 | expect(response).to redirect_to "/"
357 | end
358 | end
359 | end
360 | end
361 |
362 | describe "GET expand" do
363 | context "when hashid is valid" do
364 | it "redirects to announcement page" do
365 | get "/=#{announcement.hashid}"
366 | expect(response.status_code).to eq 302
367 | expect(response).to redirect_to "/announcements/#{announcement.id}"
368 | end
369 | end
370 |
371 | context "when hashid is invalid" do
372 | it "redirecs to root url if hashid present" do
373 | get "/=invalid"
374 | expect(response.status_code).to eq 302
375 | expect(response).to redirect_to "/"
376 | end
377 |
378 | it "returns 404 if hashid is missing" do
379 | get "/="
380 | expect(response.status_code).to eq 404
381 | expect(response).not_to redirect_to "/"
382 | end
383 | end
384 | end
385 |
386 | describe "GET random" do
387 | before { announcement }
388 |
389 | it "redirects to a random announcement" do
390 | get "/announcements/random"
391 | expect(response.status_code).to eq 302
392 | expect(response).to redirect_to "/announcements/#{announcement.id}"
393 | end
394 | end
395 | end
396 |
--------------------------------------------------------------------------------
/spec/controllers/error_controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe Amber::Controller::Error do
4 | describe "GET index" do
5 | it "returns 404 not found" do
6 | get "/index"
7 | expect(response.status_code).to eq 404
8 | expect(response.body).to eq "404 - Page not found"
9 | end
10 |
11 | it "returns 404 not found for any unknown request" do
12 | get "/blah-no-exists"
13 | expect(response.body).to eq "404 - Page not found"
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/controllers/oauth_controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 | require "webmock"
3 |
4 | describe OAuthController do
5 | describe "GET authenticate" do
6 | context "when provider is not known" do
7 | it "redirects to /" do
8 | get "/oauth/facebook"
9 | expect(response).to redirect_to "/"
10 | end
11 | end
12 | end
13 |
14 | context "Github" do
15 | let(:code) { "5ca3797f33399346a01c" }
16 | let(:github_id) { "roCDAM4ShR" }
17 | let(:github_secret) { "J9SYlLlO6XIk5lBqtOpoQlOhcbVEqbm5" }
18 | let(:github_access_token_response) do
19 | {
20 | access_token: "iuxcxlkbwe2342lkasdfjk2klsdj",
21 | token_type: "Bearer",
22 | expires_in: 300,
23 | refresh_token: nil,
24 | scope: "user",
25 | }
26 | end
27 | let(:github_user_response) do
28 | {
29 | login: "amber",
30 | id: 36345345,
31 | name: "Amber Framework",
32 | email: "test@email.com",
33 | bio: "Blah",
34 | }
35 | end
36 |
37 | let(:stub_github_authorize_request) do
38 | MultiAuth.config "github", github_id, github_secret
39 |
40 | body = "client_id=#{github_id}&client_secret=#{github_secret}&redirect_uri=&grant_type=authorization_code&code=#{code}"
41 | headers = {"Accept" => "application/json", "Content-type" => "application/x-www-form-urlencoded"}
42 |
43 | WebMock.stub(:post, "https://github.com/login/oauth/access_token")
44 | .with(body: body, headers: headers)
45 | .to_return(body: github_access_token_response.to_json)
46 |
47 | WebMock.stub(:get, "https://api.github.com/user").to_return(body: github_user_response.to_json)
48 | end
49 |
50 | describe "GET new" do
51 | it "redirects to github authorize uri" do
52 | get "/oauth/new"
53 | expect(response.headers["Location"].includes? "https://github.com/login/oauth/authorize").to be_true
54 | end
55 | end
56 |
57 | describe "GET authenticate" do
58 | before { Announcement.clear; User.clear }
59 | before { stub_github_authorize_request }
60 |
61 | it "creates a new user" do
62 | get "/oauth/github", body: "code=#{code}"
63 | u = User.find_by_uid_and_provider(github_user_response[:id], "github")
64 | expect(u).not_to be_nil
65 | expect(u.not_nil!.login).to eq github_user_response[:login]
66 | expect(u.not_nil!.name).to eq github_user_response[:name]
67 | end
68 |
69 | it "signs in a user" do
70 | get "/oauth/github", body: "code=#{code}"
71 | u = User.find_by_uid_and_provider(github_user_response[:id], "github")
72 | expect(session["user_id"]).to eq u.not_nil!.id.to_s
73 | end
74 |
75 | it "redirects to announcements#new" do
76 | get "/oauth/github", body: "code=#{code}"
77 | expect(response.status_code).to eq 302
78 | expect(response).to redirect_to "/announcements/new"
79 | end
80 |
81 | it "can find existing user and update attributes" do
82 | u = user(
83 | name: "Marilyn Manson",
84 | login: github_user_response[:login],
85 | uid: github_user_response[:id].to_s,
86 | provider: "github"
87 | ).tap(&.save).not_nil!
88 |
89 | get "/oauth/github", body: "code=#{code}"
90 | user = User.find(u.id)
91 | expect(user.not_nil!.name).to eq github_user_response[:name]
92 | end
93 |
94 | it "does not create a new user if such user exists" do
95 | u = user(
96 | name: "Marilyn Manson",
97 | login: github_user_response[:login],
98 | uid: github_user_response[:id].to_s,
99 | provider: "github"
100 | ).tap(&.save).not_nil!
101 |
102 | get "/oauth/github", body: "code=#{code}"
103 | expect(User.all.size).to eq 1
104 | end
105 | end
106 | end
107 |
108 | context "Twitter" do
109 | let(:user) { user(login: "JohnDoe").tap &.save }
110 | let(:twitter_consumer_key) { "consumer_key" }
111 | let(:twitter_consumer_secret) { "consumer_secret_key" }
112 | let(:twitter_access_token_response) do
113 | {
114 | oauth_token: "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0",
115 | oauth_token_secret: "veNRnAWe6inFuo8o2u8SLLZLjolYDmDP7SzL0YfYI",
116 | oauth_callback_confirmed: "true",
117 | }
118 | end
119 | let(:twitter_access_token_response) do
120 | {
121 | oauth_token: "7588892-kagSNqWge8gB1WwE3plnFsJHAZVfxWD7Vb57p0b4",
122 | oauth_token_secret: "PbKfYqSryyeKDWz4ebtY3o5ogNLG11WJuZBc9fQrQo",
123 | }
124 | end
125 | let(:twitter_verify_credentials_response) do
126 | {
127 | id: 38895958,
128 | name: "Sean Cook",
129 | screen_name: "theSeanCook",
130 | location: "San Francisco",
131 | url: "http://twitter.com",
132 | description: "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter.",
133 | profile_image_url: "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG",
134 | email: "me@twitter.com",
135 | }
136 | end
137 | let(:twitter_authenticate_request) do
138 | {
139 | oauth_token: "token",
140 | oauth_verifier: "verifier",
141 | }
142 | end
143 |
144 | let(:stub_twitter_authorize_request) do
145 | WebMock.stub(:post, "https://api.twitter.com/oauth/request_token")
146 | .to_return(body: HTTP::Params.encode twitter_access_token_response)
147 | end
148 | let(:stub_access_token_request) do
149 | WebMock.stub(:post, "https://api.twitter.com/oauth/access_token")
150 | .to_return(body: HTTP::Params.encode twitter_access_token_response)
151 | end
152 | let(:stub_twitter_verify_credentials_request) do
153 | WebMock.stub(:get, "https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true")
154 | .to_return(body: twitter_verify_credentials_response.to_json)
155 | end
156 |
157 | before do
158 | MultiAuth.config "twitter", twitter_consumer_key, twitter_consumer_secret
159 | end
160 |
161 | describe "GET new" do
162 | before { stub_twitter_authorize_request }
163 |
164 | it "redirects to twitter authorize uri" do
165 | get "/oauth/new?provider=twitter"
166 | location = response.headers["Location"]
167 | expect(location.includes? "https://api.twitter.com/oauth/authorize").to be_true
168 | end
169 | end
170 |
171 | describe "GET authenticate" do
172 | before { stub_access_token_request }
173 | before { stub_twitter_verify_credentials_request }
174 | before { Announcement.clear; User.clear }
175 |
176 | let(:request_body) { HTTP::Params.encode(twitter_authenticate_request) }
177 |
178 | context "when signed in" do
179 | before { login_as user }
180 |
181 | it "saves a twitter handle" do
182 | get "/oauth/twitter", body: request_body
183 | u = User.find(user.id)
184 | expect(u.try &.handle).to eq twitter_verify_credentials_response[:screen_name]
185 | end
186 |
187 | it "redirects to /me" do
188 | get "/oauth/twitter", body: request_body
189 | expect(response).to redirect_to "/me"
190 | end
191 | end
192 |
193 | context "when not signed in" do
194 | it "redirects to /" do
195 | get "/oauth/twitter", body: request_body
196 | expect(response).to redirect_to "/"
197 | end
198 | end
199 | end
200 | end
201 | end
202 |
--------------------------------------------------------------------------------
/spec/controllers/rss_controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe RSSController do
4 | let(:count) { 5 }
5 | let(:title) { "Crystal released: 1.0." }
6 | let(:user) { user(login: "crystal-lang").tap &.save }
7 |
8 | it "returns application/xml header" do
9 | get "/rss"
10 | expect(response.headers["Content-Type"]?).to eq "application/xml"
11 | end
12 |
13 | describe "GET show" do
14 | context "when there are announcements available" do
15 | it "returns newest announcements" do
16 | count.times { |i| announcement(user, title: title + i.to_s).tap &.save }
17 | get "/rss"
18 | count.times { |i| expect(response.body.includes? title + i.to_s).to be_true }
19 | end
20 | end
21 |
22 | context "when there no announcements available" do
23 | it "renders show template" do
24 | get "/rss"
25 | expect(response.status_code).to eq 200
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/controllers/session_controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe SessionController do
4 | describe "DELETE destroy" do
5 | let(:user) { user(login: "Bro").tap &.save }
6 | before { login_as user }
7 |
8 | it "signs out user" do
9 | delete "/sessions"
10 | expect(session["user_id"]).to be_nil
11 | end
12 |
13 | it "does not sign out user if csrf is invalid" do
14 | token = Base64.encode "invalid-token"
15 | delete "/sessions", body: "_csrf=#{token}"
16 | expect(response.status_code).to eq 403
17 | expect(session["user_id"]).not_to be_nil
18 | end
19 |
20 | it "redirects to root url" do
21 | delete "/sessions"
22 | expect(response.status_code).to eq 302
23 | expect(response).to redirect_to "/"
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/controllers/spec_helper.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 | require "../../config/routes"
3 | require "../../src/helpers/**"
4 | require "../../src/controllers/**"
5 |
6 | class Global
7 | @@response : HTTP::Client::Response?
8 | @@session : Amber::Router::Session::AbstractStore?
9 | @@cookies : Amber::Router::Cookies::Store?
10 |
11 | def self.response=(@@response)
12 | end
13 |
14 | def self.response
15 | @@response
16 | end
17 |
18 | def self.cookies(headers = HTTP::Headers.new)
19 | @@cookies = Amber::Router::Cookies::Store.new
20 | @@cookies.try &.update(Amber::Router::Cookies::Store.from_headers(headers))
21 | @@cookies.not_nil!
22 | end
23 |
24 | def self.session
25 | @@session ||= Amber::Router::Session::Store.new(cookies, Amber.settings.session).build
26 | end
27 | end
28 |
29 | {% for method in %w(get head post put patch delete) %}
30 | def {{method.id}}(path, headers : HTTP::Headers? = nil, body : String? = nil)
31 | request = HTTP::Request.new("{{method.id}}".upcase, path, headers, body)
32 | request.headers["Content-Type"] = Amber::Router::ParamsParser::URL_ENCODED_FORM
33 | Global.response = process_request request
34 | end
35 | {% end %}
36 |
37 | def process_request(request)
38 | io = IO::Memory.new
39 | response = HTTP::Server::Response.new(io)
40 | context = HTTP::Server::Context.new(request, response)
41 | context.session = Global.session if Global.session
42 | context.params["_csrf"] ||= Amber::Pipe::CSRF.token(context).to_s
43 | main_handler = build_main_handler
44 | main_handler.call context
45 | response.close
46 | io.rewind
47 | client_response = HTTP::Client::Response.from_io(io, decompress: false)
48 | Global.response = client_response
49 | end
50 |
51 | def build_main_handler
52 | handler = Amber::Pipe::Pipeline.new
53 | handler.build :web do
54 | plug Amber::Pipe::Error.new
55 | plug Amber::Pipe::Session.new
56 | plug Amber::Pipe::CSRF.new
57 | end
58 | handler.build :static do
59 | plug Amber::Pipe::Error.new
60 | plug Amber::Pipe::Static.new("./public")
61 | plug HTTP::CompressHandler.new
62 | end
63 | handler.prepare_pipelines
64 | handler
65 | end
66 |
67 | def response
68 | Global.response.not_nil!
69 | end
70 |
71 | def session
72 | Global.session.not_nil!
73 | end
74 |
75 | def login_as(user : User)
76 | raise ArgumentError.new("user has to be saved") if user.id.nil?
77 |
78 | session["user_id"] = user.id.to_s
79 | end
80 |
--------------------------------------------------------------------------------
/spec/controllers/static_controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe StaticController do
4 | describe "GET about" do
5 | it "renders about page" do
6 | get "/about"
7 | expect(response.status_code).to eq 200
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/controllers/user_controller_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe UserController do
4 | let(:user) { user(login: "UserControllerTest").tap &.save }
5 |
6 | before do
7 | Announcement.clear
8 | User.clear
9 | end
10 |
11 | describe "GET #show" do
12 | it "renders show template if user is found" do
13 | get "/users/#{user.login}"
14 | expect(response.status_code).to eq 200
15 | expect(response.body.includes? user.login.not_nil!).to be_true
16 | end
17 |
18 | it "redirects to root if user is not found" do
19 | get "/users/no-such-login"
20 | expect(response.status_code).to eq 302
21 | expect(response).to redirect_to "/"
22 | end
23 | end
24 |
25 | describe "GET #me" do
26 | it "renders user#show template if user is signed in" do
27 | login_as user
28 | get "/me"
29 | expect(response.status_code).to eq 200
30 | expect(response.body.includes? user.login.not_nil!).to be_true
31 | end
32 |
33 | it "redirects to root if user is not signed in" do
34 | get "/me"
35 | expect(response.status_code).to eq 302
36 | expect(response).to redirect_to "/"
37 | end
38 | end
39 |
40 | describe "PUT #remove_handle" do
41 | let(:user) { user(handle: "crystal_ann").tap &.save }
42 |
43 | it "removes user handle if user is signed in" do
44 | login_as user
45 | put "/users/remove_handle"
46 | expect(User.find(user.id).not_nil!.handle).to be_nil
47 | end
48 |
49 | it "does not remove handle if csrf token is invalid" do
50 | login_as user
51 | token = Base64.encode "invalid-token"
52 | put "/users/remove_handle", body: "_csrf=#{token}"
53 | expect(response.status_code).to eq 403
54 | expect(User.find(user.id).try &.handle).not_to be_nil
55 | end
56 |
57 | it "redirects to /me if user is signed in" do
58 | login_as user
59 | put "/users/remove_handle"
60 | expect(response.status_code).to eq 302
61 | expect(response).to redirect_to "/me"
62 | end
63 |
64 | it "redirects to / if user is not signed in" do
65 | put "/users/remove_handle"
66 | expect(response.status_code).to eq 302
67 | expect(response).to redirect_to "/"
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/spec/helpers/page_title_helper_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 | require "../../src/helpers/page_title_helper"
3 |
4 | class DumbController
5 | include Helpers::PageTitleHelper
6 | end
7 |
8 | describe Helpers::PageTitleHelper do
9 | describe "#page_title" do
10 | it "sets page title" do
11 | controller = DumbController.new
12 | controller.page_title "my page title"
13 | expect(controller.page_title).to eq "my page title - #{SITE.name}"
14 | end
15 |
16 | it "returns default site title with suffix" do
17 | expect(DumbController.new.page_title).to eq "#{SITE.description} - #{SITE.name}"
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/helpers/query_helper_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 | require "../../src/helpers/query_helper"
3 |
4 | class DumbController
5 | include Helpers::QueryHelper
6 |
7 | property params = HTTP::Params.new({} of String => Array(String))
8 | end
9 |
10 | describe Helpers::QueryHelper do
11 | describe "#to_query" do
12 | it "encodes empty params when query is empty" do
13 | expect(DumbController.new.to_query).to eq ""
14 | end
15 |
16 | it "encodes params when query is empty" do
17 | controller = DumbController.new
18 | controller.params = HTTP::Params.parse "foo=bar&qux=zoo"
19 | expect(controller.to_query).to eq "foo=bar&qux=zoo"
20 | end
21 |
22 | it "encodes params and query" do
23 | controller = DumbController.new
24 | controller.params = HTTP::Params.parse "foo=bar&qux=zoo"
25 | expect(controller.to_query page: 22).to eq "foo=bar&qux=zoo&page=22"
26 | end
27 |
28 | it "encodes params and query and overrides params values" do
29 | controller = DumbController.new
30 | controller.params = HTTP::Params.parse "foo=bar&qux=zoo&page=1"
31 | expect(controller.to_query page: 22).to eq "foo=bar&qux=zoo&page=22"
32 | end
33 |
34 | it "encodes only query when params are empty" do
35 | controller = DumbController.new
36 | expect(controller.to_query page: 22, foo: "bar").to eq "page=22&foo=bar"
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/helpers/time_ago_helper_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 | require "../../src/helpers/time_ago_helper"
3 |
4 | describe Helpers::TimeAgoHelper do
5 | describe ".time_ago_in_words" do
6 | it "transforms time interval into words" do
7 | expectations = {
8 | 1.second.ago => "just now",
9 | 15.seconds.ago => "just now",
10 | 60.seconds.ago => "a minute ago",
11 | 1.minute.ago => "a minute ago",
12 | 2.minutes.ago => "2 minutes ago",
13 | 59.minutes.ago => "59 minutes ago",
14 | 1.hour.ago => "an hour ago",
15 | 5.hour.ago => "5 hours ago",
16 | 24.hours.ago => "a day ago",
17 | 2.days.ago => "2 days ago",
18 | 1.week.ago => "a week ago",
19 | 2.weeks.ago => "2 weeks ago",
20 | 1.month.ago => "a month ago",
21 | 6.weeks.ago => "a month ago",
22 | 2.months.ago => "2 months ago",
23 | 8.months.ago => "8 months ago",
24 | 12.months.ago => "a year ago",
25 | 1.year.ago => "a year ago",
26 | 18.months.ago => "a year ago",
27 | 2.years.ago => "2 years ago",
28 | 26.months.ago => "2 years ago",
29 | 10.years.ago => "10 years ago",
30 | }
31 | expectations.each do |date, result|
32 | expect(Helpers::TimeAgoHelper.time_ago_in_words(date)).to eq result
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/initializers/database_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe DBSettings do
4 | {% for env in %w(test development staging production) %}
5 |
6 | it "has database_url in {{env.id}} environment" do
7 | db = DBSettings.load {{env}}
8 | expect(db.database_url.blank?).to eq false
9 | end
10 |
11 | {% end %}
12 | end
13 |
--------------------------------------------------------------------------------
/spec/initializers/site_spec.cr:
--------------------------------------------------------------------------------
1 | require "../spec_helper"
2 |
3 | describe SiteSettings do
4 | it "loads SITE settings" do
5 | expect(SITE).not_to be_nil
6 | end
7 |
8 | it "loads SITE.name" do
9 | expect(SITE.name.blank?).to be_false
10 | end
11 |
12 | it "loads SITE.description" do
13 | expect(SITE.description.blank?).to be_false
14 | end
15 |
16 | it "loads SITE.url" do
17 | expect(SITE.url.blank?).to be_false
18 | end
19 |
20 | {% for env in %w(test development staging production) %}
21 | let(:site) { SiteSettings.load {{env}} }
22 |
23 | it "has name in {{env.id}} environment" do
24 | expect(site.name.blank?).to be_false
25 | end
26 |
27 | it "has description in {{env.id}} environment" do
28 | expect(site.description.blank?).to be_false
29 | end
30 |
31 | it "has url in {{env.id}} environment" do
32 | expect(site.url.blank?).to be_false
33 | end
34 | {% end %}
35 | end
36 |
--------------------------------------------------------------------------------
/spec/models/announcement_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 | require "../../src/models/announcement.cr"
3 |
4 | describe Announcement do
5 | describe "Validation" do
6 | it "succeeds on valid parameters" do
7 | expect(announcement.valid?).to be_true
8 | end
9 |
10 | it "requires title" do
11 | expect(announcement(title: "").valid?).to be_false
12 | end
13 |
14 | it "validates minimum size of title" do
15 | expect(announcement(title: "-" * 3).valid?).to be_false
16 | end
17 |
18 | it "validates maximum size of title" do
19 | expect(announcement(title: "-" * 101).valid?).to be_false
20 | end
21 |
22 | it "requires description" do
23 | expect(announcement(description: "").valid?).to be_false
24 | end
25 |
26 | it "validates minimum size of description" do
27 | expect(announcement(description: "-" * 3).valid?).to be_false
28 | end
29 |
30 | it "validates maximum size of description" do
31 | expect(announcement(description: "-" * 4001).valid?).to be_false
32 | end
33 |
34 | it "validates type" do
35 | expect(announcement(type: -1_i64).valid?).to be_false
36 | end
37 | end
38 |
39 | describe "#typename" do
40 | it "returns the properly capitalized type name" do
41 | expect(announcement(type: 0_i64).typename).to eq "Blog post"
42 | expect(announcement(type: 1_i64).typename).to eq "Project update"
43 | expect(announcement(type: 2_i64).typename).to eq "Conference"
44 | expect(announcement(type: 3_i64).typename).to eq "Meetup"
45 | expect(announcement(type: 4_i64).typename).to eq "Podcast"
46 | expect(announcement(type: 5_i64).typename).to eq "Screencast"
47 | expect(announcement(type: 6_i64).typename).to eq "Video"
48 | expect(announcement(type: 7_i64).typename).to eq "Other"
49 | end
50 |
51 | it "raises error if type is wrong" do
52 | raise_error { announcement(type: -1_i64).typename }
53 | end
54 | end
55 |
56 | describe "#content" do
57 | it "returns html content" do
58 | expect(announcement(description: "test").content).to eq "
test
"
59 | end
60 |
61 | it "encodes html tags" do
62 | ann = announcement(description: "")
63 | expect(ann.content).to eq "
<script>console.log('hello')</script>
"
64 | end
65 |
66 | it "autolinks" do
67 | anns = [
68 | announcement(description: "http://www.myproj.com/"),
69 | announcement(description: "Some new project at http://www.myproj.com/."),
70 | announcement(description: "(link: http://www.myproj.com/)."),
71 | announcement(description: "# Bigger example\nLorem ipsum http://www.myproj.com/."),
72 | ]
73 | results = [
74 | "