mako_rss 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|