mako_rss 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +33 -0
- data/.rspec +2 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +38 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +124 -0
- data/Rakefile +13 -0
- data/bin/mako +6 -0
- data/lib/mako/article.rb +36 -0
- data/lib/mako/cli.rb +49 -0
- data/lib/mako/commands/build.rb +33 -0
- data/lib/mako/commands/new.rb +28 -0
- data/lib/mako/commands/schedule.rb +12 -0
- data/lib/mako/commands/version.rb +11 -0
- data/lib/mako/configuration.rb +44 -0
- data/lib/mako/core.rb +81 -0
- data/lib/mako/core_ext/numeric.rb +10 -0
- data/lib/mako/core_ext/time.rb +10 -0
- data/lib/mako/core_ext.rb +4 -0
- data/lib/mako/errors.rb +25 -0
- data/lib/mako/feed.rb +29 -0
- data/lib/mako/feed_constructor.rb +71 -0
- data/lib/mako/feed_requester.rb +43 -0
- data/lib/mako/file_open_util.rb +19 -0
- data/lib/mako/html_renderer.rb +30 -0
- data/lib/mako/layouts/_feed_container.html.erb +19 -0
- data/lib/mako/mako_logger.rb +12 -0
- data/lib/mako/sass_renderer.rb +30 -0
- data/lib/mako/subscription_list_parser.rb +28 -0
- data/lib/mako/version.rb +5 -0
- data/lib/mako/view_helpers.rb +28 -0
- data/lib/mako/writer.rb +18 -0
- data/lib/mako.rb +41 -0
- data/lib/templates/Gemfile +5 -0
- data/lib/templates/config.yaml +14 -0
- data/lib/templates/sample_subscriptions/subscriptions.json +1 -0
- data/lib/templates/sample_subscriptions/subscriptions.txt +5 -0
- data/lib/templates/sample_subscriptions/subscriptions.xml +12 -0
- data/lib/templates/themes/sass/_fonts.scss +38 -0
- data/lib/templates/themes/sass/_layout.scss +97 -0
- data/lib/templates/themes/sass/_reboot.scss +473 -0
- data/lib/templates/themes/sass/_utilities.scss +13 -0
- data/lib/templates/themes/sass/_variables.scss +57 -0
- data/lib/templates/themes/simple.html.erb +46 -0
- data/lib/templates/themes/simple.scss +6 -0
- data/mako_rss.gemspec +36 -0
- metadata +233 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mako
|
4
|
+
class FeedConstructor
|
5
|
+
attr_reader :feed_url, :feed_data
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@feed_url = args.fetch(:feed_url)
|
9
|
+
@feed_data = args.fetch(:feed_data)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Parses raw XML feed and creates Feed and Article objects to
|
13
|
+
# be rendered. Returns false if feed cannot be parsed.
|
14
|
+
#
|
15
|
+
# @return [Feed]
|
16
|
+
def parse_and_create
|
17
|
+
parsed_feed = parse_feed
|
18
|
+
return false unless parsed_feed
|
19
|
+
feed = create_feed(parsed_feed)
|
20
|
+
create_articles(feed, parsed_feed)
|
21
|
+
feed
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# @private
|
27
|
+
# Takes raw XML and parses it into a Feedjira::Feed object
|
28
|
+
#
|
29
|
+
# @return [Feedjira::Feed]
|
30
|
+
def parse_feed
|
31
|
+
Feedjira::Feed.parse(feed_data)
|
32
|
+
rescue Feedjira::NoParserAvailable
|
33
|
+
Mako.errors.add_error "Unable to parse #{feed_url}."
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates new Mako::Feed object from the parsed Feedjira::Feed object
|
38
|
+
#
|
39
|
+
# @param [Feedjira::Feed]
|
40
|
+
# @return [Mako::Feed]
|
41
|
+
def create_feed(parsed_feed)
|
42
|
+
Feed.new(url: parsed_feed.url, title: parsed_feed.title)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Creates new Mako::Article objects from the parsed Feedjira::Feed object
|
46
|
+
# if the source article was published within the last day. Adds the
|
47
|
+
# Mako::Article objects to the Mako::Feed object's articles attribute.
|
48
|
+
#
|
49
|
+
# @param [Mako::Feed] feed
|
50
|
+
# @param [Feedjira::Feed] parsed_feed
|
51
|
+
def create_articles(feed, parsed_feed)
|
52
|
+
parsed_feed.entries.each do |entry|
|
53
|
+
next unless entry.published >= (Time.now - 1.day).beginning_of_day
|
54
|
+
feed.articles << Article.new(title: entry.title,
|
55
|
+
published: entry.published,
|
56
|
+
summary: entry_summary(entry),
|
57
|
+
url: entry.url)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Atom and RSS Feedjira::Feed objects have different names for the
|
62
|
+
# article body. Returns entry.content if Atom and entry.summary if
|
63
|
+
# RSS.
|
64
|
+
#
|
65
|
+
# @param [Feedjira::Feed]
|
66
|
+
# @return [String] an HTML string of the source article body
|
67
|
+
def entry_summary(entry)
|
68
|
+
entry.class.to_s.include?('Atom') ? entry.content : entry.summary
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mako
|
4
|
+
class FeedRequester
|
5
|
+
attr_reader :feed_url
|
6
|
+
attr_accessor :ok, :body
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@ok = true
|
10
|
+
@body = ''
|
11
|
+
@feed_url = args.fetch(:feed_url)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Performs HTTP request on the given feed_url. Sets the Mako::FeedRequester
|
15
|
+
# body attribute equal to the request body if successful and returns self.
|
16
|
+
# If the request fails, @ok is set to false.
|
17
|
+
#
|
18
|
+
# @return [Mako::FeedRequester]
|
19
|
+
def fetch
|
20
|
+
begin
|
21
|
+
request = Faraday.get(feed_url)
|
22
|
+
rescue Faraday::Error
|
23
|
+
Mako.errors.add_error "Could not complete request to #{feed_url}."
|
24
|
+
self.ok = false
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
unless request.status == 200
|
28
|
+
Mako.errors.add_error "Request to #{feed_url} returned #{request.status}."
|
29
|
+
self.ok = false
|
30
|
+
return self
|
31
|
+
end
|
32
|
+
self.body = request.body
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Predicate method returning the value of @ok
|
37
|
+
#
|
38
|
+
# @return [Boolean]
|
39
|
+
def ok?
|
40
|
+
ok
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FileOpenUtil
|
4
|
+
# Opens and reads the passed in file. Thanks to the following article
|
5
|
+
# for explaining how to define a method on the included singleton class:
|
6
|
+
# https://6ftdan.com/allyourdev/2015/02/24/writing-methods-for-both-class-and-instance-levels/
|
7
|
+
#
|
8
|
+
# @param [String] resource the path to the resource
|
9
|
+
# @return [String] the opened resource
|
10
|
+
def self.included(base)
|
11
|
+
def base.load_resource(resource)
|
12
|
+
File.open(resource, 'rb', encoding: 'utf-8', &:read)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_resource(resource)
|
17
|
+
self.class.load_resource(resource)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mako
|
4
|
+
class HTMLRenderer
|
5
|
+
include FileOpenUtil
|
6
|
+
|
7
|
+
attr_reader :template, :bound, :feed_template
|
8
|
+
|
9
|
+
def initialize(args)
|
10
|
+
@template = args.fetch(:template, File.expand_path(File.join('themes', "#{Mako.config.theme}.html.erb"), Dir.pwd))
|
11
|
+
@bound = args.fetch(:bound)
|
12
|
+
@feed_template = File.expand_path('../layouts/_feed_container.html.erb', __FILE__)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Wrapper for ERB renderer. Creates new ERB instance with view template
|
16
|
+
# and renders it with binding from core.
|
17
|
+
#
|
18
|
+
# @return [String]
|
19
|
+
def render
|
20
|
+
ERB.new(load_resource(template)).result(bound.get_binding)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Default file name for rendererd file.
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
def file_path
|
27
|
+
'index.html'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<section id="feed-container">
|
2
|
+
<% @feeds.each do |feed| %>
|
3
|
+
<% if feed.articles.size > 0 %>
|
4
|
+
<section>
|
5
|
+
<header class="article-header">
|
6
|
+
<h1><a href="<%= feed.url %>"><%= h(feed.title) %></a></h1>
|
7
|
+
</header>
|
8
|
+
<% feed.articles.each do |article| %>
|
9
|
+
<article>
|
10
|
+
<h3><a href="<%= article.url %>"><%= h(article.title) %></a></h3>
|
11
|
+
<p class="meta"><%= article.formatted_published %></p>
|
12
|
+
<%= article.summary %>
|
13
|
+
</article>
|
14
|
+
<hr />
|
15
|
+
<% end %>
|
16
|
+
</section>
|
17
|
+
<% end %>
|
18
|
+
<% end %>
|
19
|
+
</section>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mako
|
4
|
+
class MakoLogger < Logger
|
5
|
+
def initialize(logdev, shift_age = 0, shift_size = 1_048_576)
|
6
|
+
super(logdev, shift_age, shift_size)
|
7
|
+
self.formatter = proc do |_severity, _datetime, _progname, msg|
|
8
|
+
"#{msg}\n"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mako
|
4
|
+
class SassRenderer
|
5
|
+
include FileOpenUtil
|
6
|
+
|
7
|
+
attr_reader :template
|
8
|
+
|
9
|
+
def initialize(args)
|
10
|
+
@template = args.fetch(:template, File.expand_path(File.join('themes', "#{Mako.config.theme}.scss"), Dir.pwd))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Wrapper for Sass::Engine. Creates new Sass::Engine instance with main
|
14
|
+
# Sass file and renders it.
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
def render
|
18
|
+
Sass::Engine.new(load_resource(template), syntax: :scss,
|
19
|
+
load_paths: [File.expand_path('themes/', Dir.pwd)],
|
20
|
+
style: :compressed).render
|
21
|
+
end
|
22
|
+
|
23
|
+
# Default file name for rendererd file.
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
def file_path
|
27
|
+
'main.css'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mako
|
4
|
+
class SubscriptionListParser
|
5
|
+
include FileOpenUtil
|
6
|
+
|
7
|
+
attr_reader :list
|
8
|
+
|
9
|
+
def initialize(args)
|
10
|
+
@list = args.fetch(:list)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parses OPML, JSON, or plain text documents and returns an Array of feed urls.
|
14
|
+
#
|
15
|
+
# @return [Array]
|
16
|
+
def parse
|
17
|
+
loaded_list = load_resource(list)
|
18
|
+
case File.extname list
|
19
|
+
when '.xml' || '.opml'
|
20
|
+
Nokogiri::XML(loaded_list).xpath('//@xmlUrl').map(&:value)
|
21
|
+
when '.json'
|
22
|
+
JSON.parse(loaded_list)
|
23
|
+
when '.txt'
|
24
|
+
loaded_list.split("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/mako/version.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewHelpers
|
4
|
+
# Returns today's date in Day, Date Month Year format
|
5
|
+
#
|
6
|
+
# @return [String]
|
7
|
+
def today
|
8
|
+
Time.now.strftime('%A, %d %B %Y')
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns the current time in month day year hour:minute:second format
|
12
|
+
#
|
13
|
+
# @return [String]
|
14
|
+
def last_updated
|
15
|
+
Time.now.strftime('%d %b %Y %H:%M:%S')
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns a string with anchor tag links to each Feed generated on the page
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
def quick_nav(feeds)
|
22
|
+
feeds.select { |feed| feed.articles.size.positive? }
|
23
|
+
.each_with_index.inject('') do |string, (feed, index)|
|
24
|
+
string += "<a href='#feed-#{index}' class='quick-nav-item'>#{feed.title} <div class='circle'>#{feed.articles.size}</div></a>"
|
25
|
+
string
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/mako/writer.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mako
|
4
|
+
class Writer
|
5
|
+
attr_reader :renderer, :destination
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@renderer = args.fetch(:renderer)
|
9
|
+
@destination = args.fetch(:destination)
|
10
|
+
end
|
11
|
+
|
12
|
+
def write
|
13
|
+
File.open(destination, 'w+', encoding: 'utf-8') do |f|
|
14
|
+
f.write(renderer.render)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/mako.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'erb'
|
5
|
+
require 'logger'
|
6
|
+
require 'faraday'
|
7
|
+
require 'feedjira'
|
8
|
+
require 'nokogiri'
|
9
|
+
require 'json'
|
10
|
+
require 'sass'
|
11
|
+
|
12
|
+
require_relative 'mako/core_ext'
|
13
|
+
require_relative 'mako/errors'
|
14
|
+
require_relative 'mako/file_open_util'
|
15
|
+
require_relative 'mako/mako_logger'
|
16
|
+
require_relative 'mako/view_helpers'
|
17
|
+
require_relative 'mako/configuration'
|
18
|
+
require_relative 'mako/subscription_list_parser'
|
19
|
+
require_relative 'mako/feed'
|
20
|
+
require_relative 'mako/article'
|
21
|
+
require_relative 'mako/feed_requester'
|
22
|
+
require_relative 'mako/feed_constructor'
|
23
|
+
require_relative 'mako/html_renderer'
|
24
|
+
require_relative 'mako/sass_renderer'
|
25
|
+
require_relative 'mako/writer'
|
26
|
+
require_relative 'mako/core'
|
27
|
+
require_relative 'mako/cli'
|
28
|
+
|
29
|
+
module Mako
|
30
|
+
def self.logger
|
31
|
+
@logger ||= MakoLogger.new(STDOUT)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.config
|
35
|
+
@config ||= Configuration.load(File.expand_path('config.yaml', Dir.pwd))
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.errors
|
39
|
+
@errors ||= Errors.new
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
---
|
2
|
+
# This configuration file is where you can change settings for your
|
3
|
+
# Mako site.
|
4
|
+
|
5
|
+
# To change the theme, uncomment the next line and change the name of the
|
6
|
+
# theme. Be sure that your .html.erb and .scss files both match the
|
7
|
+
# name of the theme.
|
8
|
+
|
9
|
+
# theme: simple
|
10
|
+
|
11
|
+
# By default, images are removed from any articles from your feeds. If you
|
12
|
+
# want images to be loaded in, change this next line to false.
|
13
|
+
|
14
|
+
# sanitize_images: true
|
@@ -0,0 +1 @@
|
|
1
|
+
["https://jonathanpike.net/feed.xml","https://daringfireball.net/feeds/main","https://marco.org/rss","https://www.speedshop.co/feed.xml","https://tenderlovemaking.com/atom.xml"]
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<opml version="2.0">
|
2
|
+
<body>
|
3
|
+
<outline text="Subscriptions" title="Subscriptions">
|
4
|
+
<outline xmlUrl='https://jonathanpike.net/feed.xml' />
|
5
|
+
<outline xmlUrl='https://daringfireball.net/feeds/main' />
|
6
|
+
<outline xmlUrl='https://marco.org/rss' />
|
7
|
+
<outline xmlUrl='https://www.speedshop.co/feed.xml' />
|
8
|
+
<outline xmlUrl='https://tenderlovemaking.com/atom.xml' />
|
9
|
+
|
10
|
+
</outline>
|
11
|
+
</body>
|
12
|
+
</opml>
|
@@ -0,0 +1,38 @@
|
|
1
|
+
// Headers
|
2
|
+
h1, h2, h3, h4, h5, h6,
|
3
|
+
.h1, .h2, .h3, .h4, .h5, .h6 {
|
4
|
+
margin-top: $headings-margin-top;
|
5
|
+
margin-bottom: $headings-margin-bottom;
|
6
|
+
font-family: $headings-font-family;
|
7
|
+
font-weight: $headings-font-weight;
|
8
|
+
line-height: $headings-line-height;
|
9
|
+
color: $headings-color;
|
10
|
+
}
|
11
|
+
|
12
|
+
h1, .h1 { font-size: $font-size-h1; }
|
13
|
+
h2, .h2 { font-size: $font-size-h2; }
|
14
|
+
h3, .h3 { font-size: $font-size-h3; }
|
15
|
+
h4, .h4 { font-size: $font-size-h4; }
|
16
|
+
h5, .h5 { font-size: $font-size-h5; }
|
17
|
+
h6, .h6 { font-size: $font-size-h6; }
|
18
|
+
|
19
|
+
// Horizontal Rules
|
20
|
+
hr {
|
21
|
+
margin-top: 1rem;
|
22
|
+
margin-bottom: 1rem;
|
23
|
+
border: 0;
|
24
|
+
border-top: 1px solid rgba($pd-black,.1);
|
25
|
+
}
|
26
|
+
|
27
|
+
// Blockquotes
|
28
|
+
blockquote {
|
29
|
+
padding: (1rem / 2) 1rem;
|
30
|
+
margin-bottom: 1rem;
|
31
|
+
font-size: $font-size-base * 1.1;
|
32
|
+
color: $pd-light-gray;
|
33
|
+
border-left: .15rem solid $pd-banner;
|
34
|
+
}
|
35
|
+
|
36
|
+
.bold {
|
37
|
+
font-weight: bold;
|
38
|
+
}
|
@@ -0,0 +1,97 @@
|
|
1
|
+
body {
|
2
|
+
font-family: $pd-font-family-sans-serif;
|
3
|
+
font-size: $font-size-base;
|
4
|
+
background-color: $pd-off-white;
|
5
|
+
margin: 0;
|
6
|
+
}
|
7
|
+
|
8
|
+
a:link {
|
9
|
+
border-bottom: 1px black dotted;
|
10
|
+
text-decoration: none;
|
11
|
+
color: $pd-black;
|
12
|
+
transition: all 0.2s ease;
|
13
|
+
-webkit-transition: all 0.2s ease;
|
14
|
+
}
|
15
|
+
|
16
|
+
a:visited {
|
17
|
+
text-decoration: none;
|
18
|
+
color: $pd-black;
|
19
|
+
}
|
20
|
+
|
21
|
+
a:hover{
|
22
|
+
text-decoration: none;
|
23
|
+
color: $pd-dark-blue;
|
24
|
+
}
|
25
|
+
|
26
|
+
.meta {
|
27
|
+
color: $pd-light-gray;
|
28
|
+
}
|
29
|
+
|
30
|
+
#banner {
|
31
|
+
background-color: $pd-banner;
|
32
|
+
text-align: center;
|
33
|
+
color: $pd-white;
|
34
|
+
h4 {
|
35
|
+
font-style: italic;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
.banner-text {
|
40
|
+
display: inline-block;
|
41
|
+
}
|
42
|
+
|
43
|
+
#quick-nav {
|
44
|
+
background-color: $pd-banner-light;
|
45
|
+
max-width: 650px;
|
46
|
+
margin: auto;
|
47
|
+
margin-top:5px;
|
48
|
+
padding: 5px;
|
49
|
+
text-align: center;
|
50
|
+
@media only screen and (max-width: $screen-phone) {
|
51
|
+
margin-top: 0px;
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
.quick-nav-item {
|
56
|
+
display: inline-block;
|
57
|
+
padding: 3px;
|
58
|
+
margin: 0 0.25em;
|
59
|
+
}
|
60
|
+
|
61
|
+
a.quick-nav-item {
|
62
|
+
border-bottom: 0px;
|
63
|
+
color: $pd-white;
|
64
|
+
}
|
65
|
+
|
66
|
+
a.quick-nav-item:hover {
|
67
|
+
color: $pd-banner-dark;
|
68
|
+
}
|
69
|
+
|
70
|
+
.circle {
|
71
|
+
display: inline-block;
|
72
|
+
background: #84BE62;
|
73
|
+
padding: 3px 9px;
|
74
|
+
border: 2px solid $pd-white;
|
75
|
+
border-radius: 100px;
|
76
|
+
}
|
77
|
+
|
78
|
+
.back-to-top {
|
79
|
+
text-align: right;
|
80
|
+
}
|
81
|
+
|
82
|
+
#feed-container {
|
83
|
+
max-width: 700px;
|
84
|
+
margin: auto;
|
85
|
+
padding: 1em;
|
86
|
+
}
|
87
|
+
|
88
|
+
.article-header {
|
89
|
+
margin-left: -1em;
|
90
|
+
}
|
91
|
+
|
92
|
+
#footer {
|
93
|
+
border-top: 1px dotted $pd-light-gray;
|
94
|
+
text-align: center;
|
95
|
+
padding-top: 3em;
|
96
|
+
margin-bottom: 3em;
|
97
|
+
}
|