mako_rss 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +33 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +7 -0
  5. data/.rubocop_todo.yml +38 -0
  6. data/.travis.yml +7 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +124 -0
  10. data/Rakefile +13 -0
  11. data/bin/mako +6 -0
  12. data/lib/mako/article.rb +36 -0
  13. data/lib/mako/cli.rb +49 -0
  14. data/lib/mako/commands/build.rb +33 -0
  15. data/lib/mako/commands/new.rb +28 -0
  16. data/lib/mako/commands/schedule.rb +12 -0
  17. data/lib/mako/commands/version.rb +11 -0
  18. data/lib/mako/configuration.rb +44 -0
  19. data/lib/mako/core.rb +81 -0
  20. data/lib/mako/core_ext/numeric.rb +10 -0
  21. data/lib/mako/core_ext/time.rb +10 -0
  22. data/lib/mako/core_ext.rb +4 -0
  23. data/lib/mako/errors.rb +25 -0
  24. data/lib/mako/feed.rb +29 -0
  25. data/lib/mako/feed_constructor.rb +71 -0
  26. data/lib/mako/feed_requester.rb +43 -0
  27. data/lib/mako/file_open_util.rb +19 -0
  28. data/lib/mako/html_renderer.rb +30 -0
  29. data/lib/mako/layouts/_feed_container.html.erb +19 -0
  30. data/lib/mako/mako_logger.rb +12 -0
  31. data/lib/mako/sass_renderer.rb +30 -0
  32. data/lib/mako/subscription_list_parser.rb +28 -0
  33. data/lib/mako/version.rb +5 -0
  34. data/lib/mako/view_helpers.rb +28 -0
  35. data/lib/mako/writer.rb +18 -0
  36. data/lib/mako.rb +41 -0
  37. data/lib/templates/Gemfile +5 -0
  38. data/lib/templates/config.yaml +14 -0
  39. data/lib/templates/sample_subscriptions/subscriptions.json +1 -0
  40. data/lib/templates/sample_subscriptions/subscriptions.txt +5 -0
  41. data/lib/templates/sample_subscriptions/subscriptions.xml +12 -0
  42. data/lib/templates/themes/sass/_fonts.scss +38 -0
  43. data/lib/templates/themes/sass/_layout.scss +97 -0
  44. data/lib/templates/themes/sass/_reboot.scss +473 -0
  45. data/lib/templates/themes/sass/_utilities.scss +13 -0
  46. data/lib/templates/themes/sass/_variables.scss +57 -0
  47. data/lib/templates/themes/simple.html.erb +46 -0
  48. data/lib/templates/themes/simple.scss +6 -0
  49. data/mako_rss.gemspec +36 -0
  50. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mako
4
+ VERSION = '0.1.0'
5
+ end
@@ -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
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'mako'
@@ -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,5 @@
1
+ https://jonathanpike.net/feed.xml
2
+ https://daringfireball.net/feeds/main
3
+ https://marco.org/rss
4
+ https://www.speedshop.co/feed.xml
5
+ 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
+ }