├── .ruby-version ├── config.ru ├── .gitignore ├── assets ├── images │ ├── bg.png │ ├── logo.png │ ├── taylor_swift01.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-144x144.png │ └── logo.svg ├── css │ ├── bourbon │ │ ├── css3 │ │ │ ├── _appearance.scss │ │ │ ├── _box-shadow.scss │ │ │ ├── _user-select.scss │ │ │ ├── _background-size.scss │ │ │ ├── _box-sizing.scss │ │ │ ├── _inline-block.scss │ │ │ ├── _perspective.scss │ │ │ ├── _image-rendering.scss │ │ │ ├── _transform.scss │ │ │ ├── _hidpi-media-query.scss │ │ │ ├── _border-radius.scss │ │ │ ├── _font-face.scss │ │ │ ├── _columns.scss │ │ │ ├── _flex-box.scss │ │ │ ├── _transition.scss │ │ │ ├── _animation.scss │ │ │ ├── _background-image.scss │ │ │ ├── _linear-gradient.scss │ │ │ ├── _border-image.scss │ │ │ ├── _radial-gradient.scss │ │ │ └── _background.scss │ │ ├── functions │ │ │ ├── _linear-gradient.scss │ │ │ ├── _compact.scss │ │ │ ├── _tint-shade.scss │ │ │ ├── _grid-width.scss │ │ │ ├── _render-gradients.scss │ │ │ ├── _transition-property-name.scss │ │ │ ├── _modular-scale.scss │ │ │ ├── _flex-grid.scss │ │ │ ├── _deprecated-webkit-gradient.scss │ │ │ └── _radial-gradient.scss │ │ ├── addons │ │ │ ├── _font-family.scss │ │ │ ├── _hide-text.scss │ │ │ ├── _clearfix.scss │ │ │ └── _prefixer.scss │ │ └── _bourbon.scss │ └── app.css └── js │ └── app.js ├── spec ├── spec_helper.rb └── app_spec.rb ├── data ├── images.json └── quotes.json ├── Gemfile ├── README.md ├── app.rb ├── Gemfile.lock ├── lib └── image.rb └── views └── index.erb /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.4 2 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require "./app" 2 | run App 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config/newrelic.yml 2 | log 3 | .rspec 4 | 5 | -------------------------------------------------------------------------------- /assets/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csampson/proverbinatus/HEAD/assets/images/bg.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csampson/proverbinatus/HEAD/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/taylor_swift01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csampson/proverbinatus/HEAD/assets/images/taylor_swift01.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csampson/proverbinatus/HEAD/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csampson/proverbinatus/HEAD/assets/images/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csampson/proverbinatus/HEAD/assets/images/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csampson/proverbinatus/HEAD/assets/images/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_appearance.scss: -------------------------------------------------------------------------------- 1 | @mixin appearance ($value) { 2 | @include prefixer(appearance, $value, webkit moz ms o spec); 3 | } 4 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_box-shadow.scss: -------------------------------------------------------------------------------- 1 | @mixin box-shadow ($shadows...) { 2 | @include prefixer(box-shadow, $shadows, webkit spec); 3 | } 4 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_user-select.scss: -------------------------------------------------------------------------------- 1 | @mixin user-select($arg: none) { 2 | @include prefixer(user-select, $arg, webkit moz ms spec); 3 | } 4 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_background-size.scss: -------------------------------------------------------------------------------- 1 | @mixin background-size ($lengths...) { 2 | @include prefixer(background-size, $lengths, webkit moz ms o spec); 3 | } 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rack/test' 2 | require File.expand_path '../../app.rb', __FILE__ 3 | 4 | include Rack::Test::Methods 5 | def app() @app||= App.new end 6 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_box-sizing.scss: -------------------------------------------------------------------------------- 1 | @mixin box-sizing ($box) { 2 | // content-box | border-box | inherit 3 | @include prefixer(box-sizing, $box, webkit moz spec); 4 | } 5 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_linear-gradient.scss: -------------------------------------------------------------------------------- 1 | @function linear-gradient($gradients...) { 2 | $type: linear; 3 | $type-gradient: append($type, $gradients, comma); 4 | 5 | @return $type-gradient; 6 | } 7 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_inline-block.scss: -------------------------------------------------------------------------------- 1 | // Legacy support for inline-block in IE7 (maybe IE6) 2 | @mixin inline-block { 3 | display: inline-block; 4 | vertical-align: baseline; 5 | zoom: 1; 6 | *display: inline; 7 | *vertical-align: auto; 8 | } 9 | -------------------------------------------------------------------------------- /data/images.json: -------------------------------------------------------------------------------- 1 | { 2 | "taylor_swift": { 3 | "file": "assets/images/taylor_swift01.png", 4 | "text_width": 451, 5 | "text_height": 210, 6 | "font_family": "arial", 7 | "font_size": 26, 8 | "column_width": 35 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_compact.scss: -------------------------------------------------------------------------------- 1 | // Remove `false` values from a list 2 | 3 | @function compact($vars...) { 4 | $list: (); 5 | @each $var in $vars { 6 | @if $var { 7 | $list: append($list, $var, comma); 8 | } 9 | } 10 | @return $list; 11 | } 12 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_perspective.scss: -------------------------------------------------------------------------------- 1 | @mixin perspective($depth: none) { 2 | // none | 3 | @include prefixer(perspective, $depth, webkit moz o spec); 4 | } 5 | 6 | @mixin perspective-origin($value: 50% 50%) { 7 | @include prefixer(perspective-origin, $value, webkit moz o spec); 8 | } 9 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_tint-shade.scss: -------------------------------------------------------------------------------- 1 | // Add percentage of white to a color 2 | @function tint($color, $percent){ 3 | @return mix(white, $color, $percent); 4 | } 5 | 6 | // Add percentage of black to a color 7 | @function shade($color, $percent){ 8 | @return mix(black, $color, $percent); 9 | } 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby "3.4.4" 3 | 4 | gem "rack", "~> 2.2" 5 | gem "rack-protection", "~> 3.2" 6 | gem "rmagick", "~> 6.0" 7 | gem "rspec", "~> 3.13" 8 | gem "sinatra", "~> 3.2" 9 | gem "sprockets", "~> 4.2" 10 | gem "thin", "~> 1.8" 11 | gem "tilt", "~> 2.4" 12 | gem 'execjs', '~> 2.9' 13 | gem 'uglifier', '~> 4.2' 14 | -------------------------------------------------------------------------------- /assets/css/bourbon/addons/_font-family.scss: -------------------------------------------------------------------------------- 1 | $georgia: Georgia, Cambria, "Times New Roman", Times, serif; 2 | $helvetica: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | $lucida-grande: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif; 4 | $monospace: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; 5 | $verdana: Verdana, Geneva, sans-serif; 6 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_image-rendering.scss: -------------------------------------------------------------------------------- 1 | @mixin image-rendering ($mode:optimizeQuality) { 2 | 3 | @if ($mode == optimize-contrast) { 4 | image-rendering: -moz-crisp-edges; 5 | image-rendering: -o-crisp-edges; 6 | image-rendering: -webkit-optimize-contrast; 7 | image-rendering: optimize-contrast; 8 | } 9 | 10 | @else { 11 | image-rendering: $mode; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /assets/css/bourbon/addons/_hide-text.scss: -------------------------------------------------------------------------------- 1 | @mixin hide-text { 2 | background-color: transparent; 3 | border: 0; 4 | color: transparent; 5 | font: 0/0 a; 6 | text-shadow: none; 7 | } 8 | 9 | // A CSS image replacement method that does not require the use of text-indent. 10 | // 11 | // Examples 12 | // 13 | // .ir { 14 | // @include hide-text; 15 | // } 16 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_transform.scss: -------------------------------------------------------------------------------- 1 | @mixin transform($property: none) { 2 | // none | 3 | @include prefixer(transform, $property, webkit moz ms o spec); 4 | } 5 | 6 | @mixin transform-origin($axes: 50%) { 7 | // x-axis - left | center | right | length | % 8 | // y-axis - top | center | bottom | length | % 9 | // z-axis - length 10 | @include prefixer(transform-origin, $axes, webkit moz ms o spec); 11 | } 12 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_grid-width.scss: -------------------------------------------------------------------------------- 1 | @function grid-width($n) { 2 | @return $n * $gw-column + ($n - 1) * $gw-gutter; 3 | } 4 | 5 | // The $gw-column and $gw-gutter variables must be defined in your base stylesheet to properly use the grid-width function. 6 | // 7 | // $gw-column: 100px; // Column Width 8 | // $gw-gutter: 40px; // Gutter Width 9 | // 10 | // div { 11 | // width: grid-width(4); // returns 520px; 12 | // margin-left: $gw-gutter; // returns 40px; 13 | // } 14 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_hidpi-media-query.scss: -------------------------------------------------------------------------------- 1 | // HiDPI mixin. Default value set to 1.3 to target Google Nexus 7 (http://bjango.com/articles/min-device-pixel-ratio/) 2 | @mixin hidpi($ratio: 1.3) { 3 | @media only screen and (-webkit-min-device-pixel-ratio: $ratio), 4 | only screen and (min--moz-device-pixel-ratio: $ratio), 5 | only screen and (-o-min-device-pixel-ratio: #{$ratio}/1), 6 | only screen and (min-resolution: #{round($ratio*96)}dpi), 7 | only screen and (min-resolution: #{$ratio}dppx) { 8 | @content; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_render-gradients.scss: -------------------------------------------------------------------------------- 1 | // User for linear and radial gradients within background-image or border-image properties 2 | 3 | @function render-gradients($gradients, $gradient-type, $vendor: false) { 4 | $vendor-gradients: false; 5 | @if $vendor { 6 | $vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient($gradients); 7 | } 8 | 9 | @else if $vendor == false { 10 | $vendor-gradients: "#{$gradient-type}-gradient(#{$gradients})"; 11 | $vendor-gradients: unquote($vendor-gradients); 12 | } 13 | @return $vendor-gradients; 14 | } 15 | -------------------------------------------------------------------------------- /assets/css/bourbon/addons/_clearfix.scss: -------------------------------------------------------------------------------- 1 | // Micro clearfix provides an easy way to contain floats without adding additional markup 2 | // 3 | // Example usage: 4 | // 5 | // // Contain all floats within .wrapper 6 | // .wrapper { 7 | // @include clearfix; 8 | // .content, 9 | // .sidebar { 10 | // float : left; 11 | // } 12 | // } 13 | 14 | @mixin clearfix { 15 | *zoom: 1; 16 | 17 | &:before, 18 | &:after { 19 | content: " "; 20 | display: table; 21 | } 22 | 23 | &:after { 24 | clear: both; 25 | } 26 | } 27 | 28 | // Acknowledgements 29 | // Micro clearfix: [Nicolas Gallagher](http://nicolasgallagher.com/micro-clearfix-hack/) 30 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_transition-property-name.scss: -------------------------------------------------------------------------------- 1 | // Return vendor-prefixed property names if appropriate 2 | // Example: transition-property-names((transform, color, background), moz) -> -moz-transform, color, background 3 | //************************************************************************// 4 | @function transition-property-names($props, $vendor: false) { 5 | $new-props: (); 6 | 7 | @each $prop in $props { 8 | $new-props: append($new-props, transition-property-name($prop, $vendor), comma); 9 | } 10 | 11 | @return $new-props; 12 | } 13 | 14 | @function transition-property-name($prop, $vendor: false) { 15 | // put other properties that need to be prefixed here aswell 16 | @if $vendor and $prop == transform { 17 | @return unquote('-'+$vendor+'-'+$prop); 18 | } 19 | @else { 20 | @return $prop; 21 | } 22 | } -------------------------------------------------------------------------------- /assets/css/bourbon/addons/_prefixer.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Example: @include prefixer(border-radius, $radii, webkit ms spec); 3 | //************************************************************************// 4 | @mixin prefixer ($property, $value, $prefixes) { 5 | 6 | @each $prefix in $prefixes { 7 | 8 | @if $prefix == webkit { 9 | -webkit-#{$property}: $value; 10 | } 11 | @else if $prefix == moz { 12 | -moz-#{$property}: $value; 13 | } 14 | @else if $prefix == ms { 15 | -ms-#{$property}: $value; 16 | } 17 | @else if $prefix == o { 18 | -o-#{$property}: $value; 19 | } 20 | @else if $prefix == spec { 21 | #{$property}: $value; 22 | } 23 | @else { 24 | @warn "Unrecognized prefix: #{$prefix}"; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path '../spec_helper.rb', __FILE__ 2 | 3 | describe 'App' do 4 | quotes = JSON.parse(File.read(File.dirname(__FILE__) + '/../quotes.json'), :symbolize_names => true) 5 | 6 | it 'can load front-end' do 7 | get '/' 8 | last_response.should be_ok 9 | end 10 | 11 | it 'can return all quotes' do 12 | get '/quotes' 13 | last_response.should be_ok 14 | end 15 | 16 | it 'can return a random quote' do 17 | get '/quotes/random' 18 | last_response.should be_ok 19 | end 20 | 21 | it 'can return a random quote of a specific topic' do 22 | get '/quotes/random/heresy' 23 | last_response.should be_ok 24 | end 25 | 26 | it 'should have quotes with citations' do 27 | quotes.any?{|q| !q[:citation] }.should == false 28 | end 29 | 30 | it 'should have quotes with text' do 31 | quotes.any?{|q| !q[:text] }.should == false 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_border-radius.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Shorthand Border-radius mixins 3 | //************************************************************************// 4 | @mixin border-top-radius($radii) { 5 | @include prefixer(border-top-left-radius, $radii, spec); 6 | @include prefixer(border-top-right-radius, $radii, spec); 7 | } 8 | 9 | @mixin border-bottom-radius($radii) { 10 | @include prefixer(border-bottom-left-radius, $radii, spec); 11 | @include prefixer(border-bottom-right-radius, $radii, spec); 12 | } 13 | 14 | @mixin border-left-radius($radii) { 15 | @include prefixer(border-top-left-radius, $radii, spec); 16 | @include prefixer(border-bottom-left-radius, $radii, spec); 17 | } 18 | 19 | @mixin border-right-radius($radii) { 20 | @include prefixer(border-top-right-radius, $radii, spec); 21 | @include prefixer(border-bottom-right-radius, $radii, spec); 22 | } 23 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_font-face.scss: -------------------------------------------------------------------------------- 1 | @mixin font-face($font-family, $file-path, $weight: normal, $style: normal, $asset-pipeline: false ) { 2 | @font-face { 3 | font-family: $font-family; 4 | font-weight: $weight; 5 | font-style: $style; 6 | 7 | @if $asset-pipeline == true { 8 | src: font-url('#{$file-path}.eot'); 9 | src: font-url('#{$file-path}.eot?#iefix') format('embedded-opentype'), 10 | font-url('#{$file-path}.woff') format('woff'), 11 | font-url('#{$file-path}.ttf') format('truetype'), 12 | font-url('#{$file-path}.svg##{$font-family}') format('svg'); 13 | } @else { 14 | src: url('#{$file-path}.eot'); 15 | src: url('#{$file-path}.eot?#iefix') format('embedded-opentype'), 16 | url('#{$file-path}.woff') format('woff'), 17 | url('#{$file-path}.ttf') format('truetype'), 18 | url('#{$file-path}.svg##{$font-family}') format('svg'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Proverbinatus 2 | 3 | ## Overview 4 | 5 | An app to display and fetch [Warhammer 40,000](http://en.wikipedia.org/wiki/Warhammer_40,000) proverbs (an Imperial 'Thought for the day'). 6 | 7 | ## The Stack 8 | 9 | `sinatra` - web framework 10 | `thin` - web server 11 | `sprockets` - static asset middleware 12 | `rmagick` - image generation/manipulation library 13 | `rspec` - BDD testing framework 14 | 15 | ## Data Storage 16 | 17 | Proverbs are stored as JSON in a flat manner, with the following keys: 18 | - `text` the actual quote text 19 | - `citation` source of the quote 20 | - `topics` themes of the proverb(e.g, 'hatred') 21 | 22 | ## API 23 | 24 | - `/quotes` returns an array of all quotes(as JSON) 25 | - `/quotes/random` returns a random quote as plain text(try this: `curl http://proverbinatus.com/quotes/random/; echo`) 26 | - `/quotes/random/:topic` returns a random quote matching the topic param 27 | - `/generate-image/taylor_swift` returns an image of Taylor Swift with a provreb embedded within 28 | 29 | --- 30 | 31 | 32 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_modular-scale.scss: -------------------------------------------------------------------------------- 1 | @function modular-scale($value, $increment, $ratio) { 2 | @if $increment > 0 { 3 | @for $i from 1 through $increment { 4 | $value: ($value * $ratio); 5 | } 6 | } 7 | 8 | @if $increment < 0 { 9 | $increment: abs($increment); 10 | @for $i from 1 through $increment { 11 | $value: ($value / $ratio); 12 | } 13 | } 14 | 15 | @return $value; 16 | } 17 | 18 | // div { 19 | // Increment Up GR with positive value 20 | // font-size: modular-scale(14px, 1, 1.618); // returns: 22.652px 21 | // 22 | // Increment Down GR with negative value 23 | // font-size: modular-scale(14px, -1, 1.618); // returns: 8.653px 24 | // 25 | // Can be used with ceil(round up) or floor(round down) 26 | // font-size: floor( modular-scale(14px, 1, 1.618) ); // returns: 22px 27 | // font-size: ceil( modular-scale(14px, 1, 1.618) ); // returns: 23px 28 | // } 29 | // 30 | // modularscale.com 31 | 32 | @function golden-ratio($value, $increment) { 33 | @return modular-scale($value, $increment, 1.618) 34 | } 35 | 36 | // div { 37 | // font-size: golden-ratio(14px, 1); // returns: 22.652px 38 | // } 39 | // 40 | // goldenratiocalculator.com 41 | -------------------------------------------------------------------------------- /assets/css/bourbon/_bourbon.scss: -------------------------------------------------------------------------------- 1 | // Custom Functions 2 | @import "functions/compact"; 3 | @import "functions/deprecated-webkit-gradient"; 4 | @import "functions/flex-grid"; 5 | @import "functions/grid-width"; 6 | @import "functions/linear-gradient"; 7 | @import "functions/modular-scale"; 8 | @import "functions/radial-gradient"; 9 | @import "functions/render-gradients"; 10 | @import "functions/tint-shade"; 11 | @import "functions/transition-property-name"; 12 | 13 | // CSS3 Mixins 14 | @import "css3/animation"; 15 | @import "css3/appearance"; 16 | @import "css3/background"; 17 | @import "css3/background-image"; 18 | @import "css3/background-size"; 19 | @import "css3/border-image"; 20 | @import "css3/border-radius"; 21 | @import "css3/box-shadow"; 22 | @import "css3/box-sizing"; 23 | @import "css3/columns"; 24 | @import "css3/flex-box"; 25 | @import "css3/font-face"; 26 | @import "css3/hidpi-media-query"; 27 | @import "css3/image-rendering"; 28 | @import "css3/inline-block"; 29 | @import "css3/linear-gradient"; 30 | @import "css3/perspective"; 31 | @import "css3/radial-gradient"; 32 | @import "css3/transform"; 33 | @import "css3/transition"; 34 | @import "css3/user-select"; 35 | 36 | // Addons & other mixins 37 | @import "addons/clearfix"; 38 | @import "addons/font-family"; 39 | @import "addons/hide-text"; 40 | @import "addons/prefixer"; 41 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | (function($, analytics) { 2 | 'use strict'; 3 | 4 | var QuoteEngine = { 5 | currentQuote: {}, 6 | _getQuote: function() { 7 | var newQuote = this.quotes[Math.floor(Math.random() * this.quotes.length)]; 8 | 9 | if(newQuote.text === this.currentQuote.text) 10 | return this._getQuote(); 11 | else 12 | return newQuote; 13 | }, 14 | // fade out, then fade back in, with new content 15 | _transition: function(element, newText) { 16 | element.stop().animate({ opacity: 0 }, function() { 17 | $(this).html(newText).animate({ opacity:1 }); 18 | }); 19 | }, 20 | init: function() { 21 | this.quoteElement = $('.proverb-body'); 22 | 23 | return $.getJSON('/quotes', function(response) { 24 | QuoteEngine.quotes = response; 25 | QuoteEngine.update(); 26 | }); 27 | }, 28 | update: function() { 29 | this.currentQuote = this._getQuote(); 30 | this._transition(this.quoteElement, this.currentQuote.text); 31 | 32 | return this; 33 | } 34 | }; 35 | 36 | $('.proverb-generate').on('click', function() { 37 | QuoteEngine.update(); 38 | analytics.push(['_trackEvent', 'quotes', 'click', 'generate']); 39 | }); 40 | 41 | QuoteEngine.init(); 42 | 43 | })(window.jQuery, window._gaq); 44 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_columns.scss: -------------------------------------------------------------------------------- 1 | @mixin columns($arg: auto) { 2 | // || 3 | @include prefixer(columns, $arg, webkit moz spec); 4 | } 5 | 6 | @mixin column-count($int: auto) { 7 | // auto || integer 8 | @include prefixer(column-count, $int, webkit moz spec); 9 | } 10 | 11 | @mixin column-gap($length: normal) { 12 | // normal || length 13 | @include prefixer(column-gap, $length, webkit moz spec); 14 | } 15 | 16 | @mixin column-fill($arg: auto) { 17 | // auto || length 18 | @include prefixer(columns-fill, $arg, webkit moz spec); 19 | } 20 | 21 | @mixin column-rule($arg) { 22 | // || || 23 | @include prefixer(column-rule, $arg, webkit moz spec); 24 | } 25 | 26 | @mixin column-rule-color($color) { 27 | @include prefixer(column-rule-color, $color, webkit moz spec); 28 | } 29 | 30 | @mixin column-rule-style($style: none) { 31 | // none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid 32 | @include prefixer(column-rule-style, $style, webkit moz spec); 33 | } 34 | 35 | @mixin column-rule-width ($width: none) { 36 | @include prefixer(column-rule-width, $width, webkit moz spec); 37 | } 38 | 39 | @mixin column-span($arg: none) { 40 | // none || all 41 | @include prefixer(column-span, $arg, webkit moz spec); 42 | } 43 | 44 | @mixin column-width($length: auto) { 45 | // auto || length 46 | @include prefixer(column-width, $length, webkit moz spec); 47 | } 48 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_flex-box.scss: -------------------------------------------------------------------------------- 1 | // CSS3 Flexible Box Model and property defaults 2 | 3 | // Custom shorthand notation for flexbox 4 | @mixin box($orient: inline-axis, $pack: start, $align: stretch) { 5 | @include display-box; 6 | @include box-orient($orient); 7 | @include box-pack($pack); 8 | @include box-align($align); 9 | } 10 | 11 | @mixin display-box { 12 | display: -webkit-box; 13 | display: -moz-box; 14 | display: box; 15 | } 16 | 17 | @mixin box-orient($orient: inline-axis) { 18 | // horizontal|vertical|inline-axis|block-axis|inherit 19 | @include prefixer(box-orient, $orient, webkit moz spec); 20 | } 21 | 22 | @mixin box-pack($pack: start) { 23 | // start|end|center|justify 24 | @include prefixer(box-pack, $pack, webkit moz spec); 25 | } 26 | 27 | @mixin box-align($align: stretch) { 28 | // start|end|center|baseline|stretch 29 | @include prefixer(box-align, $align, webkit moz spec); 30 | } 31 | 32 | @mixin box-direction($direction: normal) { 33 | // normal|reverse|inherit 34 | @include prefixer(box-direction, $direction, webkit moz spec); 35 | } 36 | 37 | @mixin box-lines($lines: single) { 38 | // single|multiple 39 | @include prefixer(box-lines, $lines, webkit moz spec); 40 | } 41 | 42 | @mixin box-ordinal-group($int: 1) { 43 | @include prefixer(box-ordinal-group, $int, webkit moz spec); 44 | } 45 | 46 | @mixin box-flex($value: 0.0) { 47 | @include prefixer(box-flex, $value, webkit moz spec); 48 | } 49 | 50 | @mixin box-flex-group($int: 1) { 51 | @include prefixer(box-flex-group, $int, webkit moz spec); 52 | } 53 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra" 2 | require "sprockets" 3 | require "uglifier" 4 | require "json" 5 | 6 | require_relative "./lib/image" 7 | 8 | class App < Sinatra::Application 9 | set :environment, Sprockets::Environment.new 10 | 11 | # append assets paths 12 | environment.append_path "assets/css" 13 | environment.append_path "assets/images" 14 | environment.append_path "assets/js" 15 | 16 | # compress assets 17 | environment.js_compressor = :uglify 18 | # environment.css_compressor = :scss 19 | 20 | # get assets 21 | get "/assets/*" do 22 | env["PATH_INFO"].sub!("/assets", "") 23 | settings.environment.call(env) 24 | end 25 | 26 | quotes = JSON.parse(File.read("./data/quotes.json"), :symbolize_names => true) 27 | images = JSON.parse(File.read("./data/images.json"), :symbolize_names => true) 28 | 29 | get "/" do 30 | erb :index 31 | end 32 | 33 | get "/quotes/?" do 34 | content_type :json 35 | quotes.to_json 36 | end 37 | 38 | get "/quotes/random/?" do 39 | quotes.sample[:text] 40 | end 41 | 42 | get "/quotes/random/:topic/?" do 43 | matching_quotes = quotes.select{|quote| quote[:topics].include? params[:topic]} 44 | 45 | if matching_quotes.length > 0 46 | matching_quotes.sample[:text] 47 | else 48 | "No matching quotes found." 49 | end 50 | end 51 | 52 | get "/generate-image/:image/?" do 53 | content_type "image/png" 54 | 55 | image_config = images[params[:image].to_sym] 56 | image = Image.new quotes.sample[:text], image_config 57 | image.draw! 58 | 59 | image.to_blob 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_flex-grid.scss: -------------------------------------------------------------------------------- 1 | // Flexible grid 2 | @function flex-grid($columns, $container-columns: $fg-max-columns) { 3 | $width: $columns * $fg-column + ($columns - 1) * $fg-gutter; 4 | $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; 5 | @return percentage($width / $container-width); 6 | } 7 | 8 | // Flexible gutter 9 | @function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) { 10 | $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; 11 | @return percentage($gutter / $container-width); 12 | } 13 | 14 | // The $fg-column, $fg-gutter and $fg-max-columns variables must be defined in your base stylesheet to properly use the flex-grid function. 15 | // This function takes the fluid grid equation (target / context = result) and uses columns to help define each. 16 | // 17 | // $fg-column: 60px; // Column Width 18 | // $fg-gutter: 25px; // Gutter Width 19 | // $fg-max-columns: 12; // Total Columns For Main Container 20 | // 21 | // div { 22 | // width: flex-grid(4); // returns (315px / 1020px) = 30.882353%; 23 | // margin-left: flex-gutter(); // returns (25px / 1020px) = 2.45098%; 24 | // 25 | // p { 26 | // width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; 27 | // float: left; 28 | // margin: flex-gutter(4); // returns (25px / 315px) = 7.936508%; 29 | // } 30 | // 31 | // blockquote { 32 | // float: left; 33 | // width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; 34 | // } 35 | // } 36 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_transition.scss: -------------------------------------------------------------------------------- 1 | // Shorthand mixin. Supports multiple parentheses-deliminated values for each variable. 2 | // Example: @include transition (all, 2.0s, ease-in-out); 3 | // @include transition ((opacity, width), (1.0s, 2.0s), ease-in, (0, 2s)); 4 | // @include transition ($property:(opacity, width), $delay: (1.5s, 2.5s)); 5 | 6 | @mixin transition ($properties...) { 7 | @if length($properties) >= 1 { 8 | @include prefixer(transition, $properties, webkit moz ms o spec); 9 | } 10 | 11 | @else { 12 | $properties: all 0.15s ease-out 0; 13 | @include prefixer(transition, $properties, webkit moz ms o spec); 14 | } 15 | } 16 | 17 | @mixin transition-property ($properties...) { 18 | -webkit-transition-property: transition-property-names($properties, 'webkit'); 19 | -moz-transition-property: transition-property-names($properties, 'moz'); 20 | -ms-transition-property: transition-property-names($properties, 'ms'); 21 | -o-transition-property: transition-property-names($properties, 'o'); 22 | transition-property: transition-property-names($properties, false); 23 | } 24 | 25 | @mixin transition-duration ($times...) { 26 | @include prefixer(transition-duration, $times, webkit moz ms o spec); 27 | } 28 | 29 | @mixin transition-timing-function ($motions...) { 30 | // ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() 31 | @include prefixer(transition-timing-function, $motions, webkit moz ms o spec); 32 | } 33 | 34 | @mixin transition-delay ($times...) { 35 | @include prefixer(transition-delay, $times, webkit moz ms o spec); 36 | } 37 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_deprecated-webkit-gradient.scss: -------------------------------------------------------------------------------- 1 | // Render Deprecated Webkit Gradient - Linear || Radial 2 | //************************************************************************// 3 | @function deprecated-webkit-gradient($type, 4 | $deprecated-pos1, $deprecated-pos2, 5 | $full, 6 | $deprecated-radius1: false, $deprecated-radius2: false) { 7 | $gradient-list: (); 8 | $gradient: false; 9 | $full-length: length($full); 10 | $percentage: false; 11 | $gradient-type: $type; 12 | 13 | @for $i from 1 through $full-length { 14 | $gradient: nth($full, $i); 15 | 16 | @if length($gradient) == 2 { 17 | $color-stop: color-stop(nth($gradient, 2), nth($gradient, 1)); 18 | $gradient-list: join($gradient-list, $color-stop, comma); 19 | } 20 | 21 | @else if $gradient != null { 22 | @if $i == $full-length { 23 | $percentage: 100%; 24 | } 25 | 26 | @else { 27 | $percentage: ($i - 1) * (100 / ($full-length - 1)) + "%"; 28 | } 29 | 30 | $color-stop: color-stop(unquote($percentage), $gradient); 31 | $gradient-list: join($gradient-list, $color-stop, comma); 32 | } 33 | } 34 | 35 | @if $type == radial { 36 | $gradient: -webkit-gradient(radial, $deprecated-pos1, $deprecated-radius1, $deprecated-pos2, $deprecated-radius2, $gradient-list); 37 | } 38 | 39 | @else if $type == linear { 40 | $gradient: -webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $gradient-list); 41 | } 42 | 43 | @return $gradient; 44 | } 45 | -------------------------------------------------------------------------------- /assets/css/bourbon/functions/_radial-gradient.scss: -------------------------------------------------------------------------------- 1 | // This function is required and used by the background-image mixin. 2 | @function radial-gradient($G1, $G2, 3 | $G3: false, $G4: false, 4 | $G5: false, $G6: false, 5 | $G7: false, $G8: false, 6 | $G9: false, $G10: false, 7 | $pos: 50% 50%, 8 | $shape-size: ellipse cover) { 9 | 10 | @each $value in $G1, $G2 { 11 | $first-val: nth($value, 1); 12 | $pos-type: type-of($first-val); 13 | 14 | @if ($pos-type != color) or ($first-val != "transparent") { 15 | @if ($pos-type == number) 16 | or ($first-val == "center") 17 | or ($first-val == "top") 18 | or ($first-val == "right") 19 | or ($first-val == "bottom") 20 | or ($first-val == "left") { 21 | 22 | $pos: $value; 23 | 24 | @if $pos == $G1 { 25 | $G1: false; 26 | } 27 | } 28 | 29 | @else if 30 | ($first-val == "ellipse") 31 | or ($first-val == "circle") 32 | or ($first-val == "closest-side") 33 | or ($first-val == "closest-corner") 34 | or ($first-val == "farthest-side") 35 | or ($first-val == "farthest-corner") 36 | or ($first-val == "contain") 37 | or ($first-val == "cover") { 38 | 39 | $shape-size: $value; 40 | 41 | @if $value == $G1 { 42 | $G1: false; 43 | } 44 | 45 | @else if $value == $G2 { 46 | $G2: false; 47 | } 48 | } 49 | } 50 | } 51 | 52 | $type: radial; 53 | $gradient: compact($pos, $shape-size, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 54 | $type-gradient: append($type, $gradient, comma); 55 | 56 | @return $type-gradient; 57 | } 58 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_animation.scss: -------------------------------------------------------------------------------- 1 | // http://www.w3.org/TR/css3-animations/#the-animation-name-property- 2 | // Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties. 3 | 4 | // Official animation shorthand property. 5 | @mixin animation ($animations...) { 6 | @include prefixer(animation, $animations, webkit moz spec); 7 | } 8 | 9 | // Individual Animation Properties 10 | @mixin animation-name ($names...) { 11 | @include prefixer(animation-name, $names, webkit moz spec); 12 | } 13 | 14 | 15 | @mixin animation-duration ($times...) { 16 | @include prefixer(animation-duration, $times, webkit moz spec); 17 | } 18 | 19 | 20 | @mixin animation-timing-function ($motions...) { 21 | // ease | linear | ease-in | ease-out | ease-in-out 22 | @include prefixer(animation-timing-function, $motions, webkit moz spec); 23 | } 24 | 25 | 26 | @mixin animation-iteration-count ($values...) { 27 | // infinite | 28 | @include prefixer(animation-iteration-count, $values, webkit moz spec); 29 | } 30 | 31 | 32 | @mixin animation-direction ($directions...) { 33 | // normal | alternate 34 | @include prefixer(animation-direction, $directions, webkit moz spec); 35 | } 36 | 37 | 38 | @mixin animation-play-state ($states...) { 39 | // running | paused 40 | @include prefixer(animation-play-state, $states, webkit moz spec); 41 | } 42 | 43 | 44 | @mixin animation-delay ($times...) { 45 | @include prefixer(animation-delay, $times, webkit moz spec); 46 | } 47 | 48 | 49 | @mixin animation-fill-mode ($modes...) { 50 | // none | forwards | backwards | both 51 | @include prefixer(animation-fill-mode, $modes, webkit moz spec); 52 | } 53 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | base64 (0.3.0) 5 | concurrent-ruby (1.3.5) 6 | daemons (1.4.1) 7 | diff-lcs (1.6.2) 8 | eventmachine (1.2.7) 9 | execjs (2.10.0) 10 | logger (1.7.0) 11 | mustermann (3.0.3) 12 | ruby2_keywords (~> 0.0.1) 13 | observer (0.1.2) 14 | pkg-config (1.6.2) 15 | rack (2.2.17) 16 | rack-protection (3.2.0) 17 | base64 (>= 0.1.0) 18 | rack (~> 2.2, >= 2.2.4) 19 | rmagick (6.1.1) 20 | observer (~> 0.1) 21 | pkg-config (~> 1.4) 22 | rspec (3.13.1) 23 | rspec-core (~> 3.13.0) 24 | rspec-expectations (~> 3.13.0) 25 | rspec-mocks (~> 3.13.0) 26 | rspec-core (3.13.5) 27 | rspec-support (~> 3.13.0) 28 | rspec-expectations (3.13.5) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.13.0) 31 | rspec-mocks (3.13.5) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.13.0) 34 | rspec-support (3.13.4) 35 | ruby2_keywords (0.0.5) 36 | sinatra (3.2.0) 37 | mustermann (~> 3.0) 38 | rack (~> 2.2, >= 2.2.4) 39 | rack-protection (= 3.2.0) 40 | tilt (~> 2.0) 41 | sprockets (4.2.2) 42 | concurrent-ruby (~> 1.0) 43 | logger 44 | rack (>= 2.2.4, < 4) 45 | thin (1.8.2) 46 | daemons (~> 1.0, >= 1.0.9) 47 | eventmachine (~> 1.0, >= 1.0.4) 48 | rack (>= 1, < 3) 49 | tilt (2.6.1) 50 | uglifier (4.2.1) 51 | execjs (>= 0.3.0, < 3) 52 | 53 | PLATFORMS 54 | ruby 55 | 56 | DEPENDENCIES 57 | execjs (~> 2.9) 58 | rack (~> 2.2) 59 | rack-protection (~> 3.2) 60 | rmagick (~> 6.0) 61 | rspec (~> 3.13) 62 | sinatra (~> 3.2) 63 | sprockets (~> 4.2) 64 | thin (~> 1.8) 65 | tilt (~> 2.4) 66 | uglifier (~> 4.2) 67 | 68 | RUBY VERSION 69 | ruby 3.4.4p34 70 | 71 | BUNDLED WITH 72 | 2.6.9 73 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #212121 url('/images/bg.png') repeat; 3 | color: #EEE; 4 | text-shadow: 0 2px #000; 5 | font-family: 'Marcellus SC', sans-serif; 6 | font-size: 100%; 7 | line-height: 1.15; 8 | margin: 0; 9 | } 10 | 11 | .page { 12 | text-align: center; 13 | width: 70%; 14 | margin: 5em auto 0; 15 | } 16 | 17 | .proverb { 18 | font-size: 325%; 19 | width: 100%; 20 | height: 5.5em; 21 | margin: 0; 22 | display: table; 23 | } 24 | 25 | .proverb-body { 26 | vertical-align: middle; 27 | display: table-cell; 28 | } 29 | 30 | .proverb-generate { 31 | border-radius: 2px; 32 | box-shadow: 0 1px 1px -1px #BD6A6A inset; 33 | border: 1px solid #171717; 34 | border-bottom-color: #080808; 35 | background: linear-gradient(#671919, #501414); 36 | color: white; 37 | font: inherit; 38 | cursor: pointer; 39 | padding: 0.75em 5em; 40 | } 41 | 42 | .proverb-generate:hover, 43 | .proverb-generate:focus { 44 | outline: none; 45 | background: linear-gradient(#721C1C, #591616); 46 | } 47 | 48 | .page-footer { 49 | border-top: 1px solid #111; 50 | margin: 1em auto 0; 51 | display: table; 52 | } 53 | 54 | .page-footer::before { 55 | content: ''; 56 | border-bottom: 1px solid #333; 57 | display: block; 58 | } 59 | 60 | .page-footer-text { 61 | font-size: 85%; 62 | } 63 | 64 | .logo { 65 | margin-top: 1em; 66 | display: inline-block; 67 | } 68 | 69 | .logo-image, 70 | .logo-text { 71 | vertical-align: middle; 72 | } 73 | 74 | .logo-image { 75 | margin-right: 1ex; 76 | } 77 | 78 | .logo-text { 79 | font-size: 150%; 80 | } 81 | 82 | @media (max-width: 50em) { 83 | .page { 84 | width: auto; 85 | padding: 1em; 86 | margin-top: 1em; 87 | } 88 | 89 | .proverb { 90 | font-size: 250%; 91 | } 92 | } 93 | 94 | @media (max-width: 35em) { 95 | .proverb { 96 | font-size: 175%; 97 | } 98 | } -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_background-image.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Background-image property for adding multiple background images with 3 | // gradients, or for stringing multiple gradients together. 4 | //************************************************************************// 5 | 6 | @mixin background-image($images...) { 7 | background-image: add-prefix($images, webkit); 8 | background-image: add-prefix($images, moz); 9 | background-image: add-prefix($images, ms); 10 | background-image: add-prefix($images, o); 11 | background-image: add-prefix($images); 12 | } 13 | 14 | 15 | @function add-prefix($images, $vendor: false) { 16 | $images-prefixed: (); 17 | 18 | @for $i from 1 through length($images) { 19 | $type: type-of(nth($images, $i)); // Get type of variable - List or String 20 | 21 | // If variable is a list - Gradient 22 | @if $type == list { 23 | $gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial) 24 | $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) 25 | 26 | $gradient: render-gradients($gradient-args, $gradient-type, $vendor); 27 | $images-prefixed: append($images-prefixed, $gradient, comma); 28 | } 29 | 30 | // If variable is a string - Image 31 | @else if $type == string { 32 | $images-prefixed: join($images-prefixed, nth($images, $i), comma); 33 | } 34 | } 35 | @return $images-prefixed; 36 | } 37 | 38 | 39 | //Examples: 40 | //@include background-image(linear-gradient(top, orange, red)); 41 | //@include background-image(radial-gradient(50% 50%, cover circle, orange, red)); 42 | //@include background-image(url("/images/a.png"), linear-gradient(orange, red)); 43 | //@include background-image(url("image.png"), linear-gradient(orange, red), url("image.png")); 44 | //@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red)); 45 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_linear-gradient.scss: -------------------------------------------------------------------------------- 1 | @mixin linear-gradient($pos, $G1, $G2: false, 2 | $G3: false, $G4: false, 3 | $G5: false, $G6: false, 4 | $G7: false, $G8: false, 5 | $G9: false, $G10: false, 6 | $deprecated-pos1: left top, 7 | $deprecated-pos2: left bottom, 8 | $fallback: false) { 9 | // Detect what type of value exists in $pos 10 | $pos-type: type-of(nth($pos, 1)); 11 | 12 | // If $pos is missing from mixin, reassign vars and add default position 13 | @if ($pos-type == color) or (nth($pos, 1) == "transparent") { 14 | $G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5; 15 | $G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos; 16 | $pos: top; // Default position 17 | } 18 | 19 | $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 20 | 21 | // Set $G1 as the default fallback color 22 | $fallback-color: nth($G1, 1); 23 | 24 | // If $fallback is a color use that color as the fallback color 25 | @if (type-of($fallback) == color) or ($fallback == "transparent") { 26 | $fallback-color: $fallback; 27 | } 28 | 29 | background-color: $fallback-color; 30 | background-image: deprecated-webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $full); // Safari <= 5.0 31 | background-image: -webkit-linear-gradient($pos, $full); // Safari 5.1+, Chrome 32 | background-image: -moz-linear-gradient($pos, $full); 33 | background-image: -ms-linear-gradient($pos, $full); 34 | background-image: -o-linear-gradient($pos, $full); 35 | background-image: unquote("linear-gradient(#{$pos}, #{$full})"); 36 | } 37 | 38 | 39 | // Usage: Gradient position is optional, default is top. Position can be a degree. Color stops are optional as well. 40 | // @include linear-gradient(#1e5799, #2989d8); 41 | // @include linear-gradient(#1e5799, #2989d8, $fallback:#2989d8); 42 | // @include linear-gradient(top, #1e5799 0%, #2989d8 50%); 43 | // @include linear-gradient(50deg, rgba(10, 10, 10, 0.5) 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%); 44 | -------------------------------------------------------------------------------- /lib/image.rb: -------------------------------------------------------------------------------- 1 | require 'rmagick' 2 | 3 | class Image 4 | HEIGHT = 315 5 | WIDTH = 851 6 | 7 | attr_reader :result 8 | 9 | def initialize(text, options) 10 | @file = options[:file] 11 | @text = text 12 | @text_width = options[:text_width] 13 | @text_height = options[:text_height] 14 | @font_family = options[:font_family] 15 | @font_size = options[:font_size] 16 | @column_width = options[:column_width] 17 | end 18 | 19 | def draw! 20 | base = Magick::Image.new HEIGHT, WIDTH 21 | img = Magick::Image.read(@file).first 22 | 23 | # Add the text 24 | draw_text_on img, text: word_wrap(@text, @column_width), 25 | pointsize: @font_size, 26 | gravity: Magick::SouthEastGravity, 27 | width: @text_width, 28 | height: @text_height, 29 | font_family: @font_family 30 | 31 | # Add branding 32 | draw_text_on img, text: 'www.proverbinatus.com', 33 | pointsize: 12, 34 | gravity: Magick::SouthEastGravity, 35 | width: WIDTH, 36 | height: HEIGHT 37 | 38 | img.format = 'png' 39 | 40 | @result = img 41 | end 42 | 43 | def to_blob 44 | @result.to_blob 45 | end 46 | 47 | private 48 | def draw_text_on(image, width: 0, 49 | height: 0, 50 | x: 0, 51 | y: 0, 52 | text: '', 53 | fill_color: 'black', 54 | font_family: 'arial', 55 | pointsize: 10, 56 | gravity: CenterGravity) 57 | 58 | draw = Magick::Draw.new 59 | draw.font_family = font_family 60 | draw.pointsize = pointsize 61 | draw.gravity = gravity 62 | 63 | draw.fill = fill_color 64 | draw.annotate image, width, height, x, y, text 65 | end 66 | 67 | def word_wrap(text, columns = 80) 68 | text.split("\n").collect do |line| 69 | line.length > columns ? line.gsub(/(.{1,#{columns}})(\s+|$)/, "\\1\n").strip : line 70 | end * "\n" 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /views/index.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Proverbinatus 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 26 | 27 | 28 | 29 |
30 |
31 |

32 |
33 | 34 | 35 | 36 |
37 | 41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_border-image.scss: -------------------------------------------------------------------------------- 1 | @mixin border-image($images) { 2 | -webkit-border-image: border-add-prefix($images, webkit); 3 | -moz-border-image: border-add-prefix($images, moz); 4 | -o-border-image: border-add-prefix($images, o); 5 | border-image: border-add-prefix($images); 6 | } 7 | 8 | @function border-add-prefix($images, $vendor: false) { 9 | $border-image: (); 10 | $images-type: type-of(nth($images, 1)); 11 | $first-var: nth(nth($images, 1), 1); // Get type of Gradient (Linear || radial) 12 | 13 | // If input is a gradient 14 | @if $images-type == string { 15 | @if ($first-var == "linear") or ($first-var == "radial") { 16 | @for $i from 2 through length($images) { 17 | $gradient-type: nth($images, 1); // Get type of gradient (linear || radial) 18 | $gradient-args: nth($images, $i); // Get actual gradient (red, blue) 19 | $border-image: render-gradients($gradient-args, $gradient-type, $vendor); 20 | } 21 | } 22 | 23 | // If input is a URL 24 | @else { 25 | $border-image: $images; 26 | } 27 | } 28 | 29 | // If input is gradient or url + additional args 30 | @else if $images-type == list { 31 | @for $i from 1 through length($images) { 32 | $type: type-of(nth($images, $i)); // Get type of variable - List or String 33 | 34 | // If variable is a list - Gradient 35 | @if $type == list { 36 | $gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial) 37 | $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) 38 | $border-image: render-gradients($gradient-args, $gradient-type, $vendor); 39 | } 40 | 41 | // If variable is a string - Image or number 42 | @else if ($type == string) or ($type == number) { 43 | $border-image: append($border-image, nth($images, $i)); 44 | } 45 | } 46 | } 47 | @return $border-image; 48 | } 49 | 50 | //Examples: 51 | // @include border-image(url("image.png")); 52 | // @include border-image(url("image.png") 20 stretch); 53 | // @include border-image(linear-gradient(45deg, orange, yellow)); 54 | // @include border-image(linear-gradient(45deg, orange, yellow) stretch); 55 | // @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round); 56 | // @include border-image(radial-gradient(top, cover, orange, yellow, orange)); 57 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_radial-gradient.scss: -------------------------------------------------------------------------------- 1 | // Requires Sass 3.1+ 2 | @mixin radial-gradient($G1, $G2, 3 | $G3: false, $G4: false, 4 | $G5: false, $G6: false, 5 | $G7: false, $G8: false, 6 | $G9: false, $G10: false, 7 | $pos: 50% 50%, 8 | $shape-size: ellipse cover, 9 | $deprecated-pos1: center center, 10 | $deprecated-pos2: center center, 11 | $deprecated-radius1: 0, 12 | $deprecated-radius2: 460, 13 | $fallback: false) { 14 | 15 | @each $value in $G1, $G2 { 16 | $first-val: nth($value, 1); 17 | $pos-type: type-of($first-val); 18 | 19 | @if ($pos-type != color) or ($first-val != "transparent") { 20 | @if ($pos-type == number) 21 | or ($first-val == "center") 22 | or ($first-val == "top") 23 | or ($first-val == "right") 24 | or ($first-val == "bottom") 25 | or ($first-val == "left") { 26 | 27 | $pos: $value; 28 | 29 | @if $pos == $G1 { 30 | $G1: false; 31 | } 32 | } 33 | 34 | @else if 35 | ($first-val == "ellipse") 36 | or ($first-val == "circle") 37 | or ($first-val == "closest-side") 38 | or ($first-val == "closest-corner") 39 | or ($first-val == "farthest-side") 40 | or ($first-val == "farthest-corner") 41 | or ($first-val == "contain") 42 | or ($first-val == "cover") { 43 | 44 | $shape-size: $value; 45 | 46 | @if $value == $G1 { 47 | $G1: false; 48 | } 49 | 50 | @else if $value == $G2 { 51 | $G2: false; 52 | } 53 | } 54 | } 55 | } 56 | 57 | $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 58 | 59 | // Set $G1 as the default fallback color 60 | $first-color: nth($full, 1); 61 | $fallback-color: nth($first-color, 1); 62 | 63 | @if (type-of($fallback) == color) or ($fallback == "transparent") { 64 | $fallback-color: $fallback; 65 | } 66 | 67 | background-color: $fallback-color; 68 | background-image: deprecated-webkit-gradient(radial, $deprecated-pos1, $deprecated-pos2, $full, $deprecated-radius1, $deprecated-radius2); // Safari <= 5.0 69 | background-image: -webkit-radial-gradient($pos, $shape-size, $full); 70 | background-image: -moz-radial-gradient($pos, $shape-size, $full); 71 | background-image: -ms-radial-gradient($pos, $shape-size, $full); 72 | background-image: -o-radial-gradient($pos, $shape-size, $full); 73 | background-image: unquote("radial-gradient(#{$pos}, #{$shape-size}, #{$full})"); 74 | } 75 | 76 | // Usage: Gradient position and shape-size are required. Color stops are optional. 77 | // @include radial-gradient(50% 50%, circle cover, #1e5799, #efefef); 78 | // @include radial-gradient(50% 50%, circle cover, #eee 10%, #1e5799 30%, #efefef); 79 | -------------------------------------------------------------------------------- /assets/css/bourbon/css3/_background.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Background property for adding multiple backgrounds using shorthand 3 | // notation. 4 | //************************************************************************// 5 | 6 | @mixin background( 7 | $background-1 , $background-2: false, 8 | $background-3: false, $background-4: false, 9 | $background-5: false, $background-6: false, 10 | $background-7: false, $background-8: false, 11 | $background-9: false, $background-10: false, 12 | $fallback: false 13 | ) { 14 | $backgrounds: compact($background-1, $background-2, 15 | $background-3, $background-4, 16 | $background-5, $background-6, 17 | $background-7, $background-8, 18 | $background-9, $background-10); 19 | 20 | $fallback-color: false; 21 | @if (type-of($fallback) == color) or ($fallback == "transparent") { 22 | $fallback-color: $fallback; 23 | } 24 | @else { 25 | $fallback-color: extract-background-color($backgrounds); 26 | } 27 | 28 | @if $fallback-color { 29 | background-color: $fallback-color; 30 | } 31 | background: background-add-prefix($backgrounds, webkit); 32 | background: background-add-prefix($backgrounds, moz); 33 | background: background-add-prefix($backgrounds, ms); 34 | background: background-add-prefix($backgrounds, o); 35 | background: background-add-prefix($backgrounds); 36 | } 37 | 38 | @function extract-background-color($backgrounds) { 39 | $final-bg-layer: nth($backgrounds, length($backgrounds)); 40 | @if type-of($final-bg-layer) == list { 41 | @for $i from 1 through length($final-bg-layer) { 42 | $value: nth($final-bg-layer, $i); 43 | @if type-of($value) == color { 44 | @return $value; 45 | } 46 | } 47 | } 48 | @return false; 49 | } 50 | 51 | 52 | @function background-add-prefix($backgrounds, $vendor: false) { 53 | $backgrounds-prefixed: (); 54 | 55 | @for $i from 1 through length($backgrounds) { 56 | $shorthand: nth($backgrounds, $i); // Get member for current index 57 | $type: type-of($shorthand); // Get type of variable - List or String 58 | 59 | // If shorthand is a list 60 | @if $type == list { 61 | $first-member: nth($shorthand, 1); // Get first member of shorthand 62 | 63 | // Linear Gradient 64 | @if index(linear radial, nth($first-member, 1)) { 65 | $gradient-type: nth($first-member, 1); // linear || radial 66 | 67 | // Get actual gradient (red, blue) 68 | $gradient-args: false; 69 | $shorthand-start: false; 70 | // Linear gradient and positioning, repeat, etc. values 71 | @if type-of($first-member) == list { 72 | $gradient-args: nth($first-member, 2); 73 | $shorthand-start: 2; 74 | } 75 | // Linear gradient only 76 | @else { 77 | $gradient-args: nth($shorthand, 2); // Get actual gradient (red, blue) 78 | $shorthand-start: 3; 79 | } 80 | 81 | $gradient: render-gradients($gradient-args, $gradient-type, $vendor); 82 | @for $j from $shorthand-start through length($shorthand) { 83 | $gradient: join($gradient, nth($shorthand, $j), space); 84 | } 85 | $backgrounds-prefixed: append($backgrounds-prefixed, $gradient, comma); 86 | } 87 | 88 | // Image with additional properties 89 | @else { 90 | $backgrounds-prefixed: append($backgrounds-prefixed, $shorthand, comma); 91 | } 92 | 93 | } 94 | 95 | // If shorthand is a simple string, color or image 96 | @else if $type == string { 97 | $backgrounds-prefixed: join($backgrounds-prefixed, $shorthand, comma); 98 | } 99 | } 100 | @return $backgrounds-prefixed; 101 | } 102 | 103 | //Examples: 104 | //@include background(linear-gradient(top, orange, red)); 105 | //@include background(radial-gradient(50% 50%, cover circle, orange, red)); 106 | //@include background(url("/images/a.png") no-repeat, linear-gradient(orange, red)); 107 | //@include background(url("image.png") center center, linear-gradient(orange, red), url("image.png")); 108 | -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 72 | 75 | 80 | 85 | 90 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /data/quotes.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "text": "There is no such thing as innocence - only varying degrees of guilt.", 3 | "citation": "Codex: Black Templars(4th edition)", 4 | "topics": ["innocence", "guilt"] 5 | }, 6 | { 7 | "text": "Reason begets doubt; doubt begets heresy.", 8 | "citation": "Warhammer 40000(5th edition)", 9 | "topics": ["reason", "doubt"] 10 | }, 11 | { 12 | "text": "Only the awkward question; only the foolish ask twice.", 13 | "citation": "Codex: Space Marines(4th edition)", 14 | "topics": ["awkward", "question"] 15 | }, 16 | { 17 | "text": "Success is measured in blood; yours or your enemy's.", 18 | "citation": "Codex: Space Marines(4th edition)", 19 | "topics": ["success"] 20 | }, 21 | { 22 | "text": "The reward for treachery is retribution.", 23 | "citation": "Warhammer 40000(5th edition)", 24 | "topics": ["treachery"] 25 | }, 26 | { 27 | "text": "Hope is the beginning of unhappiness.", 28 | "citation": "Warhammer 40000(3rd edition)", 29 | "topics": ["hope"] 30 | }, 31 | { 32 | "text": "Even a man who has nothing can still offer his life.", 33 | "citation": "Warhammer 40000(5th edition)", 34 | "topics": ["sacrifice"] 35 | }, 36 | { 37 | "text": "Inspiration grows from the barrel of a gun.", 38 | "citation": "Warhammer 400000: Compendium", 39 | "topics": ["inspiration", "gun"] 40 | }, 41 | { 42 | "text": "Walk softly and carry a big gun.", 43 | "citation": "Codex: Space Marines(4th edition)", 44 | "topics": ["caution", "gun"] 45 | }, 46 | { 47 | "text": "A mind without purpose will wander in dark places.", 48 | "citation": "Warhammer 40000(3rd edition)", 49 | "topics": ["purposelessness"] 50 | }, 51 | { 52 | "text": "A moment of laxity spawns a lifetime of heresy.", 53 | "citation": "Codex: Dark Angels(3rd edition)", 54 | "topics": ["laxity"] 55 | }, 56 | { 57 | "text": "A small mind is a tidy mind.", 58 | "citation": "Warhammer 40000(3rd edition)", 59 | "topics": ["small mind"] 60 | }, 61 | { 62 | "text": "A suspicious mind is a healthy mind.", 63 | "citation": "Warhammer 40000(4th edition)", 64 | "topics": ["suspicion"] 65 | }, 66 | { 67 | "text": "An open mind is like a fortress with its gates unbarred and unguarded.", 68 | "citation": "Warhammer 40000(3rd edition)", 69 | "topics": ["open mind"] 70 | }, 71 | { 72 | "text": "Call no man happy until he is dead.", 73 | "citation": "Codex: Imperial Guard(3rd edition)", 74 | "topics": ["happiness"] 75 | }, 76 | { 77 | "text": "Death is the servant of the righteous.", 78 | "citation": "Codex: Chaos Space Marines(4th edition)", 79 | "topics": ["death"] 80 | }, 81 | { 82 | "text": "Doubt is a sign of weakness.", 83 | "citation": "Warhammer 40000(4th edition)", 84 | "topics": ["doubt"] 85 | }, 86 | { 87 | "text": "Excuses are the refuge of the weak.", 88 | "citation": "Warhammer 40000(4th edition)", 89 | "topics": ["excuses"] 90 | }, 91 | { 92 | "text": "Foolish are those who fear nothing, yet claim to know everything.", 93 | "citation": "Warhammer 40000(4th edition)", 94 | "topics": ["knowledge"] 95 | }, 96 | { 97 | "text": "For a warrior the only crime is cowardice.", 98 | "citation": "Warhammer 40000: Rogue Trader", 99 | "topics": ["cowardice"] 100 | }, 101 | { 102 | "text": "Happiness is a delusion of the weak.", 103 | "citation": "Warhammer 40000(4th edition)", 104 | "topics": ["happiness"] 105 | }, 106 | { 107 | "text": "Hate enriches.", 108 | "citation": "Warhammer 40000(4th edition)", 109 | "topics": ["hatred"] 110 | }, 111 | { 112 | "text": "Heresy grows from idleness.", 113 | "citation": "Codex: Black Templars(4th edition)", 114 | "topics": ["idleness"] 115 | }, 116 | { 117 | "text": "Success is commemorated. Failure merely remembered.", 118 | "citation": "Warhammer 40000(3rd edition)", 119 | "topics": ["success", "failure"] 120 | }, 121 | { 122 | "text": "The rewards of tolerance are treachery and betrayal.", 123 | "citation": "Warhammer 40000(4th edition)", 124 | "topics": ["tolerance"] 125 | }, 126 | { 127 | "text": "The truly wise are always afraid.", 128 | "citation": "Warhammer 40000(4th edition)", 129 | "topics": ["wisdom"] 130 | }, 131 | { 132 | "text": "Through the destruction of our enemies we earn our salvation.", 133 | "citation": "Warhammer 40000(3rd edition)", 134 | "topics": ["victory"] 135 | }, 136 | { 137 | "text": "Prayer cleanses the soul, but pain cleanses the body.", 138 | "citation": "Warhammer 40000(3rd edition)", 139 | "topics": ["prayer", "pain"] 140 | }, 141 | { 142 | "text": "Innocence proves nothing.", 143 | "citation": "Codex: Black Templars(4th edition)", 144 | "topics": ["innocence"] 145 | }, 146 | { 147 | "text": "There is nothing to fear but failure.", 148 | "citation": "Codex: Black Templars(4th edition)", 149 | "topics": ["failure"] 150 | }, 151 | { 152 | "text": "To question is to doubt.", 153 | "citation": "Warhammer 40000(4th edition)", 154 | "topics": ["question"] 155 | }, 156 | { 157 | "text": "Reason is the cloak of traitors.", 158 | "citation": "Warhammer 40000(4th edition)", 159 | "topics": ["reason"] 160 | }, 161 | { 162 | "text": "Ruthlessness is the kindness of the wise.", 163 | "citation": "Warhammer 40000(4th edition)", 164 | "topics": ["ruthlessness"] 165 | }, 166 | { 167 | "text": "Sorrow awaits the foolhardy.", 168 | "citation": "Codex: Armageddon(3rd edition)", 169 | "topics": ["fool"] 170 | }, 171 | { 172 | "text": "The burden of failure is the most terrible punishment of all.", 173 | "citation": "Warhammer 40000 4th edition", 174 | "topics": ["failure"] 175 | }, 176 | { 177 | "text": "The common man is like a worm in the gut of a corpse, trapped inside a prison of cold flesh, helpless and uncaring, unaware even of the inevitability of its own doom.", 178 | "citation": "Codex: Imperialis", 179 | "topics": ["doom"] 180 | }, 181 | { 182 | "text": "The difference between heresy and treachery is ignorance.", 183 | "citation": "Warhammer 40000(3rd edition)", 184 | "topics": ["treachery"] 185 | }, 186 | { 187 | "text": "The dissident invites only retribution.", 188 | "citation": "Warhammer 40000(4th edition)", 189 | "topics": ["dissidence"] 190 | }, 191 | { 192 | "text": "The justice of your action is measured by the strength of your conviction.", 193 | "citation": "Warhammer 40000(4th edition)", 194 | "topics": ["justice", "conviction"] 195 | }, 196 | { 197 | "text": "The keenest blade is righteous hatred.", 198 | "citation": "Warhammer 40000(4th edition)", 199 | "topics": ["hatred"] 200 | }, 201 | { 202 | "text": "The most deviant mind is often concealed in an unblemished body.", 203 | "citation": "Warhammer 40000(5th edition)", 204 | "topics": ["deviance"] 205 | }, 206 | { 207 | "text": "The mutant bears his heresy on the outside, the traitor hides it in his soul.", 208 | "citation": "Warhammer 40000(3rd edition)", 209 | "topics": ["treachery"] 210 | }, 211 | { 212 | "text": "The only reaction to treachery is vengeance.", 213 | "citation": "Warhammer 40000(3rd edition)", 214 | "topics": ["treachery"] 215 | }, 216 | { 217 | "text": "The road to purity is drenched in the blood of the martyred.", 218 | "citation": "Codex: Imperial Guard(3rd edition)", 219 | "topics": ["purity"] 220 | }, 221 | { 222 | "text": "The seed of heresy rests in the minds of reasonable men.", 223 | "citation": "Warhammer 40000(4th edition)", 224 | "topics": ["reason"] 225 | }, 226 | { 227 | "text": "The traitorʹs hand lies closer than you think.", 228 | "citation": "The Traitor's Hand(novel)", 229 | "topics": ["treachery"] 230 | }, 231 | { 232 | "text": "The universe is a big place and, whatever happens, you will not be missed...", 233 | "citation": "Warhammer 40000(5th edition)", 234 | "topics": [] 235 | }, 236 | { 237 | "text": "The wage of negligence is utter destruction.", 238 | "citation": "Codex: Armageddon(3rd edition)", 239 | "topics": ["negligence"] 240 | }, 241 | { 242 | "text": "The wise man learns from the deaths of others.", 243 | "citation": "Warhammer 40000(3rd edition)", 244 | "topics": ["wisdom"] 245 | }, 246 | { 247 | "text": "There are no answers. Only death.", 248 | "citation": "Warhammer 40000: Rogue Trader", 249 | "topics": ["answers", "death"] 250 | }, 251 | { 252 | "text": "They who feast today do so in ignorance of their mortality. Tomorrow they must die or change.", 253 | "citation": "Warhammer 40000(5th edition)", 254 | "topics": [] 255 | }, 256 | { 257 | "text": "Timidity begets indecision; indecision begets treachery.", 258 | "citation": "Warhammer 40000(4th edition)", 259 | "topics": ["timidity", "indecision"] 260 | }, 261 | { 262 | "text": "To compromise is to err.", 263 | "citation": "Warhammer 40000(4th edition)", 264 | "topics": ["compromise"] 265 | }, 266 | { 267 | "text": "To err is to invite retribution.", 268 | "citation": "Warhammer 40000: Rogue Trader", 269 | "topics": ["error"] 270 | }, 271 | { 272 | "text": "To withdraw in disgust is not apathy.", 273 | "citation": "Warhammer 40000(4th edition)", 274 | "topics": ["disgust", "apathy"] 275 | }, 276 | { 277 | "text": "Truth begets hatred.", 278 | "citation": "Codex: Black Templars(4th edition)", 279 | "topics": ["truth", "hatred"] 280 | }, 281 | { 282 | "text": "Trying to understand weakens the will to act.", 283 | "citation": "Codex: Black Templars(4th edition)", 284 | "topics": ["understanding"] 285 | }, 286 | { 287 | "text": "Victory needs no explanation, defeat allows none.", 288 | "citation": "Warhammer 40000(3rd edition)", 289 | "topics": ["victory", "defeat"] 290 | }, 291 | { 292 | "text": "Vigilance is the brother of truth.", 293 | "citation": "Codex: Necrons(3rd edition)", 294 | "topics": ["vigilance", "truth"] 295 | }, 296 | { 297 | "text": "Vigilance is your shield.", 298 | "citation": "Codex: Necrons(3rd edition)", 299 | "topics": ["vigilance"] 300 | }, 301 | { 302 | "text": "You are not required to think, only to act.", 303 | "citation": "Warhammer 40000(4th edition)", 304 | "topics": ["thought"] 305 | }, 306 | { 307 | "text": "Zeal is its own excuse.", 308 | "citation": "Warhammer 40000(4th edition)", 309 | "topics": ["zeal"] 310 | }, 311 | { 312 | "text": "A broad mind lacks focus.", 313 | "citation": "Warhammer 40000(4th edition)", 314 | "topics": ["open mind"] 315 | }, 316 | { 317 | "text": "A coward always seeks compromise.", 318 | "citation": "Warhammer 40000(4th edition)", 319 | "topics": ["cowardice", "compromise"] 320 | }, 321 | { 322 | "text": "A logical argument must be dismissed with absolute conviction!", 323 | "citation": "Warhammer 40000(4th edition)", 324 | "topics": ["logic"] 325 | }, 326 | { 327 | "text": "A questioning servant is more dangerous than an ignorant heretic.", 328 | "citation": "Warhammer 40000(4th edition)", 329 | "topics": ["question"] 330 | }, 331 | { 332 | "text": "Accept your lot!", 333 | "citation": "Warhammer 40000(4th edition)", 334 | "topics": ["fortune"] 335 | }, 336 | { 337 | "text": "An empty mind is a loyal mind.", 338 | "citation": "Codex: Craftworld Eldar(3rd edition)", 339 | "topics": ["intelligence", "loyalty"] 340 | }, 341 | { 342 | "text": "Analysis is the bane of conviction.", 343 | "citation": "Warhammer 40000(4th edition)", 344 | "topics": ["analysis"] 345 | }, 346 | { 347 | "text": "Appeasement is a curse.", 348 | "citation": "Warhammer 40000(4th edition)", 349 | "topics": ["appeasement"] 350 | }, 351 | { 352 | "text": "Be strong in your ignorance.", 353 | "citation": "Codex: Imperial Guard(3rd edition)", 354 | "topics": ["ignorance"] 355 | }, 356 | { 357 | "text": "Blessed is the mind too small for doubt.", 358 | "citation": "Warhammer 40000(3rd edition)", 359 | "topics": ["doubt"] 360 | }, 361 | { 362 | "text": "Burn the Heretic. Kill the Mutant. Purge the Unclean.", 363 | "citation": "Warhammer 40000(4th edition)", 364 | "topics": ["heresy", "mutation", "unclean"] 365 | }, 366 | { 367 | "text": "Compromise is akin to treachery.", 368 | "citation": "Warhammer 40000(4th edition)", 369 | "topics": ["compromise"] 370 | }, 371 | { 372 | "text": "Contemplation is the womb of treachery.", 373 | "citation": "Warhammer 40000(4th edition)", 374 | "topics": ["Contemplation"] 375 | }, 376 | { 377 | "text": "Death brings its own reward.", 378 | "citation": "Warhammer 40000: Rogue Trader", 379 | "topics": ["death"] 380 | }, 381 | { 382 | "text": "Death is the only answer.", 383 | "citation": "Warhammer 40000: Rogue Trader", 384 | "topics": ["death"] 385 | }, 386 | { 387 | "text": "Every man is a spark in the darkness. By the time he is noticed he is gone forever. A retinal after-image that fades and is obscured by newer, brighter lights.", 388 | "citation": "Warhammer 40000: Rogue Trader", 389 | "topics": [] 390 | }, 391 | { 392 | "text": "Facts are chains that bind perception and fetter truth. For a man can remake the world if he has a dream and no facts to cloud his mind.", 393 | "citation": "Warhammer 40000(3rd edition)", 394 | "topics": ["facts"] 395 | }, 396 | { 397 | "text": "For those who seek perfection there can be no rest this side of the grave.", 398 | "citation": "Warhammer 40000(3rd edition)", 399 | "topics": ["perfection"] 400 | }, 401 | { 402 | "text": "Forgiveness is a sign of weakness.", 403 | "citation": "Codex: Space Marines(4th edition)", 404 | "topics": ["forgiveness"] 405 | }, 406 | { 407 | "text": "In an hour of darkness a blind man is the best guide. In an age of insanity look to the madman to show the way.", 408 | "citation": "Warhammer 40000(3rd edition)", 409 | "topics": ["insanity"] 410 | }, 411 | { 412 | "text": "Intellect is a mask for traitors.", 413 | "citation": "Warhammer 40000(4th edition)", 414 | "topics": ["intellect"] 415 | }, 416 | { 417 | "text": "Intolerance is a blessing.", 418 | "citation": "Warhammer 40000(4th edition)", 419 | "topics": ["intolerance", "tolerance"] 420 | }, 421 | { 422 | "text": "Knowledge is power, hide it well.", 423 | "citation": "Warhammer 40000(3rd edition)", 424 | "topics": ["knowledge"] 425 | }, 426 | { 427 | "text": "Knowledge is to be feared!", 428 | "citation": "Warhammer 40000(4th edition)", 429 | "topics": ["knowledge"] 430 | }, 431 | { 432 | "text": "Leniency is a sign of weakness!", 433 | "citation": "Warhammer 40000(4th edition)", 434 | "topics": ["Leniency"] 435 | }, 436 | { 437 | "text": "Life is a prison, death a release.", 438 | "citation": "Codex: Black Templars(4th edition)", 439 | "topics": ["life", "death"] 440 | }, 441 | { 442 | "text": "Mercy is a sign of weakness.", 443 | "citation": "Codex: Space Marines(4th edition)", 444 | "topics": ["mercy"] 445 | }, 446 | { 447 | "text": "Negotiation is surrender.", 448 | "citation": "Warhammer 40000(4th edition)", 449 | "topics": ["Negotiation"] 450 | }, 451 | { 452 | "text": "Never forget, never forgive.", 453 | "citation": "Codex: Dark Angels(3rd edition)", 454 | "topics": ["forgiveness"] 455 | }, 456 | { 457 | "text": "Not even the dead know the end of war.", 458 | "citation": "Codex: Craftworld Eldar(3rd edition)", 459 | "topics": ["war"] 460 | }, 461 | { 462 | "text": "Only in death does duty end.", 463 | "citation": "Warhammer 40000(3rd edition)", 464 | "topics": ["duty"] 465 | }, 466 | { 467 | "text": "Only the insane have strength enough to prosper.", 468 | "citation": "Codex: Space Marines(4th edition)", 469 | "topics": ["insanity"] 470 | }, 471 | { 472 | "text": "Peace is Hell.", 473 | "citation": "Warhammer 40000: Rogue Trader", 474 | "topics": ["peace"] 475 | }, 476 | { 477 | "text": "Perseverance and silence are the highest virtues.", 478 | "citation": "Warhammer 40000(4th edition)", 479 | "topics": ["perseverance"] 480 | }, 481 | { 482 | "text": "Only the weak question.", 483 | "citation": "Imperial Armour IV", 484 | "topics": ["question"] 485 | }, 486 | { 487 | "text": "All souls cry out for salvation.", 488 | "citation": "Codex: Dark Angels(3rd Edition)", 489 | "topics": [] 490 | }, 491 | { 492 | "text": "All mortal life is folly that does not feed the spirit.", 493 | "citation": "Warhammer 40000(4th edition)", 494 | "topics": [] 495 | }, 496 | { 497 | "text": "As the mind to the body so the soul to the spirit, as death to the mortal man so failure to the immortal, such is the price of all ambition.", 498 | "citation": "Warhammer 40000(4th edition)", 499 | "topics": ["ambition"] 500 | }, 501 | { 502 | "text": "Blessed are the gun makers.", 503 | "citation": "Warhammer 40000(4th edition)", 504 | "topics": ["gun"] 505 | }, 506 | { 507 | "text": "Blind faith is a just cause.", 508 | "citation": "Warhammer 40000(4th edition)", 509 | "topics": ["faith"] 510 | }, 511 | { 512 | "text": "Curse now the death in vain.", 513 | "citation": "Codex: Space Marines(4th edition)", 514 | "topics": ["death"] 515 | }, 516 | { 517 | "text": "Damnation is eternal.", 518 | "citation": "Codex: Dark Angels(3rd Edition)", 519 | "topics": ["damnation"] 520 | }, 521 | { 522 | "text": "Dark dreams lie upon the heart.", 523 | "citation": "Warhammer 40000(4th edition)", 524 | "topics": [] 525 | }, 526 | { 527 | "text": "Fear the shadows; despise the night. There are horrors that no man can face and survive.", 528 | "citation": "Codex: Dark Eldar (3rd Edition)", 529 | "topics": [] 530 | }, 531 | { 532 | "text": "Heresy must be met with hatred.", 533 | "citation": "Warhammer 40000(4th edition)", 534 | "topics": ["heresy"] 535 | }, 536 | { 537 | "text": "Know thine enemy.", 538 | "citation": "Codex: Dark Eldar (3rd Edition)", 539 | "topics": ["enemy"] 540 | }, 541 | { 542 | "text": "Sins hidden in the heart turn all into decay.", 543 | "citation": "Warhammer 40000(4th edition)", 544 | "topics": ["sin"] 545 | }, 546 | { 547 | "text": "Claims of innocence mean nothing; they serve only to prove a foolish lack of caution.", 548 | "citation": "Codex Imperialis", 549 | "topics": ["innocence"] 550 | }, 551 | { 552 | "text": "The tools of salvation are faith and bullets.", 553 | "citation": "Warhammer 40000(6th edition)", 554 | "topics": ["salvtion", "faith"] 555 | }, 556 | { 557 | "text": "A treacherous thought is as dangerous as a hundred bullets.", 558 | "citation": "Warhammer 40000(6th edition)", 559 | "topics": ["treachery"] 560 | }, 561 | { 562 | "text": "Doubt is the fatal flaw in any armour.", 563 | "citation": "Warhammer 40000(6th edition)", 564 | "topics": ["doubt"] 565 | }, 566 | { 567 | "text": "To punish the traitor is reward in itself.", 568 | "citation": "Warhammer 40000(6th edition)", 569 | "topics": ["betrayal"] 570 | }, 571 | { 572 | "text": "Enlightenment is a myth; we do not need to understand in order to hate.", 573 | "citation": "Warhammer 40000(6th edition)", 574 | "topics": ["enlightenment"] 575 | }, 576 | { 577 | "text": "Death and duty are all we must give.", 578 | "citation": "Warhammer 40000(6th edition)", 579 | "topics": ["death", "duty"] 580 | }, 581 | { 582 | "text": "Betrayal's tithe is the hangman's noose.", 583 | "citation": "Warhammer 40000(6th edition)", 584 | "topics": ["betrayal"] 585 | }, 586 | { 587 | "text": "True happiness stems only from duty.", 588 | "citation": "Warhammer 40000(6th edition)", 589 | "topics": ["happiness", "duty"] 590 | }, 591 | { 592 | "text": "It is better for a man to be afraid than happy.", 593 | "citation": "Warhammer 40000(6th edition)", 594 | "topics": ["fear", "happiness"] 595 | }, 596 | { 597 | "text": "To search for answers is to beat a path to damnation.", 598 | "citation": "Warhammer 40000(6th edition)", 599 | "topics": ["knowledge"] 600 | }, 601 | { 602 | "text": "To err is human; To err again is treachery.", 603 | "citation": "Warhammer 40000(6th edition)", 604 | "topics": ["err"] 605 | }, 606 | { 607 | "text": "A closed mind is defence against sedition.", 608 | "citation": "Warhammer 40000(6th edition)", 609 | "topics": ["closed mind", "sedition"] 610 | }, 611 | { 612 | "text": "Knowledge is power; Power corrupts.", 613 | "citation": "Warhammer 40000(6th edition)", 614 | "topics": ["knowledge"] 615 | }, 616 | { 617 | "text": "Better crippled in body than corrupt in mind.", 618 | "citation": "Warhammer 40000(6th edition)", 619 | "topics": ["corruption"] 620 | }, 621 | { 622 | "text": "To relinquish contempt is capitulation.", 623 | "citation": "Warhammer 40000(6th edition)", 624 | "topics": ["contempt"] 625 | }, 626 | { 627 | "text": "Do not let fear conquer hate!", 628 | "citation": "Warhammer 40000(6th edition)", 629 | "topics": ["fear", "hate"] 630 | } 631 | ] --------------------------------------------------------------------------------