jekyll-recker 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.
data/README.org ADDED
@@ -0,0 +1,119 @@
1
+ #+TITLE: jekyll-recker
2
+ #+SLUG: jekyll-recker.html
3
+ #+PERMALINK: jekyll-recker.html
4
+ #+STARTUP: showall
5
+ #+DESCRIPTION: my website's custom jekyll plugin
6
+
7
+ This is the jekyll plugin for my personal website.
8
+
9
+ * Installation
10
+
11
+ Add =jekyll-recker= to the =jekyll_plugins= group of your =Gemfile=.
12
+
13
+ #+BEGIN_SRC ruby
14
+ group :jekyll_plugins do
15
+ gem 'jekyll-recker', :git => 'https://github.com/arecker/jekyll-recker.git'
16
+ end
17
+ #+END_SRC
18
+
19
+ Add =jekyll-recker= to the list of plugins in jekyll's =_config.yml=.
20
+
21
+ #+BEGIN_SRC yaml
22
+ # _config.yaml
23
+ plugins:
24
+ - jekyll-recker
25
+ #+END_SRC
26
+
27
+ Set the theme.
28
+
29
+ #+BEGIN_SRC yaml
30
+ theme: jekyll-recker
31
+ #+END_SRC
32
+
33
+ Install and enjoy.
34
+
35
+ #+BEGIN_SRC sh
36
+ bundle install
37
+ bundle exec jekyll serve
38
+ #+END_SRC
39
+
40
+ * Usage
41
+
42
+ ** Commands
43
+
44
+ *** =tweet=
45
+
46
+ The =tweet= command tweets a link to the latest published jekyll blog
47
+ post.
48
+
49
+ Ensure the following environment variables are set,.
50
+
51
+ #+BEGIN_SRC sh
52
+ export ACCESS_TOKEN_SECRET="..."
53
+ export ACCESS_TOKEN="..."
54
+ export CONSUMER_API_KEY="..."
55
+ export CONSUMER_API_SECRET="..."
56
+ #+END_SRC
57
+
58
+ Alternatively, configure which commands to run to fetch the secrets.
59
+
60
+ #+BEGIN_SRC yaml
61
+ # _config.yml
62
+ recker:
63
+ twitter:
64
+ access_token_secret_cmd: cat secrets/access-token-secret
65
+ access_token_cmd: cat secrets/access-token
66
+ consumer_api_key_cmd: cat secrets/consumer-api-key
67
+ consumer_api_secret_cmd: cat secrets/consumer-api-secret-key
68
+ #+END_SRC
69
+
70
+ Run =bundle exec jekyll tweet= to let it rip!
71
+
72
+ [[assets/images/example-tweet.png]]
73
+
74
+ ** Generators
75
+
76
+ *** =stats=
77
+
78
+ On build time, =jekyll-recker= calculates and stores the following
79
+ stats in the =site.data.stats= object, which are by default rendered in a
80
+ widget on the home page layout.
81
+
82
+ [[assets/images/example-stats.png]]
83
+
84
+ If you'd like, you can override the template with your own stats
85
+ widget by providing your own =_includes/stats.html=.
86
+
87
+ | Field Name | Field Description |
88
+ |-----------------+------------------------------------------------------|
89
+ | =posts= | The total number of published posts. |
90
+ | =words.total= | The total number of words from all published post. |
91
+ | =words.average= | The average number of words for each published post. |
92
+ | =days.days= | Current streak of daily, consecutive posts. |
93
+ | =days.start= | First day of current streak. |
94
+ | =days.end= | Last day of current streak. |
95
+
96
+ Example:
97
+
98
+ #+BEGIN_SRC html
99
+ <!-- _includes/stats.html -->
100
+
101
+ <table>
102
+ <tr>
103
+ <th>Total Posts</th>
104
+ <th>Total Words</th>
105
+ <th>Average Words per Post</th>
106
+ <th>Current Streak</th>
107
+ <th>First day of current streak</th>
108
+ <th>Last day of current streak</th>
109
+ </tr>
110
+ <tr>
111
+ <td>{{ site.data.stats.posts }}</td>
112
+ <td>{{ site.data.stats.words.total }}</td>
113
+ <td>{{ site.data.stats.words.average }}</td>
114
+ <td>{{ site.data.stats.days.days }}</td>
115
+ <td>{{ site.data.stats.days.start }}</td>
116
+ <td>{{ site.data.stats.days.end }}</td>
117
+ </tr>
118
+ </table>
119
+ #+END_SRC
@@ -0,0 +1,23 @@
1
+ <div class="ui two column stackable grid">
2
+ {%- for post in site.posts -%}
3
+ {%- capture this_month -%}{{ post.date | date: '%B %Y' }}{%- endcapture -%}
4
+ {%- capture next_month -%}{{ post.previous.date | date: '%B %Y' }}{%- endcapture -%}
5
+ {%- if forloop.first -%}
6
+ <div class="center aligned column">
7
+ <h3 class="ui large header">{{ this_month }}</h3>
8
+ <div class="ui middle aligned large link list">
9
+ {%- endif -%}
10
+ <a class="item" href="{{ post.url }}">{{ post.title }}</a>
11
+ {%- if this_month != next_month -%}
12
+ </div>
13
+ </div>
14
+ <div class="center aligned column">
15
+ <h3 class="ui large header">{{ next_month }}</h3>
16
+ <div class="ui middle aligned large link list">
17
+ {%- endif -%}
18
+ {%- if forloop.last -%}
19
+ </div>
20
+ </div>
21
+ {%- endif -%}
22
+ {%- endfor -%}
23
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="ui container">
2
+ <h1 class="ui massive dividing header">
3
+ {{ include.title }}
4
+ <div class="sub header">
5
+ {{ include.subtitle }}
6
+ </div>
7
+ </h1>
8
+ <div class="ui huge breadcrumb">
9
+ <a href="{{ site.baseurl}}/" class="section">
10
+ <i class="home icon"></i>
11
+ </a>
12
+ <i class="right angle icon divider"></i>
13
+ <div class="active section">{{ include.slug }}</div>
14
+ </div>
15
+ </div>
@@ -0,0 +1,10 @@
1
+ {%- if page.next -%}
2
+ <a href="{{ page.next.url }}" class="ui basic left floated button">
3
+ <i class="angle left icon"></i> {{ page.next.slug }}
4
+ </a>
5
+ {%- endif -%}
6
+ {%- if page.previous -%}
7
+ <a href="{{ page.previous.url }}" class="ui basic right floated button">
8
+ {{ page.previous.slug }} <i class="angle right icon"></i>
9
+ </a>
10
+ {%- endif -%}
@@ -0,0 +1,36 @@
1
+ <div class="ui center aligned four small statistics">
2
+ <div class="ui small statistic">
3
+ <div class="value">
4
+ {{ site.data.stats.words.total }}
5
+ </div>
6
+ <div class="label">
7
+ Total Words
8
+ </div>
9
+ </div>
10
+ <div class="ui small statistic">
11
+ <div class="value">
12
+ {{ site.data.stats.words.average }}
13
+ </div>
14
+ <div class="label">
15
+ Ave. Words per post
16
+ </div>
17
+ </div>
18
+ <div class="ui small statistic">
19
+ <div class="value">
20
+ {{ site.data.stats.posts }}
21
+ </div>
22
+ <div class="label">
23
+ Posts
24
+ </div>
25
+ </div>
26
+ <div class="ui small statistic">
27
+ <div class="value">
28
+ {{ site.data.stats.days.days }}
29
+ </div>
30
+ <div class="label">
31
+ Consecutive Days
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" integrity="sha256-9mbkOfVho3ZPXfM7W8sV2SndrGDuh7wuyLjtsWeTI1Q=" crossorigin="anonymous" />
7
+ <link href="/assets/jekyll-recker.css" rel="stylesheet"/>
8
+ <title>
9
+ {{ site.title }} | {% if page.title != nil %}{{ page.title }}{% else %}{{ site.description }}{% endif %}
10
+ </title>
11
+ </head>
12
+ <body>
13
+ <div class="ui container">
14
+ {{ content }}
15
+ </div>
16
+ <div class="ui vertical footer segment">
17
+ <div class="ui center aligned container">
18
+ <div class="ui horizontal large divided link list">
19
+ <div class="ui buttons">
20
+ <a class="ui circular rss basic button" href="/feed.xml"><i class="rss icon"></i></a>
21
+ <a class="ui circular email basic button" href="mailto:{{ site.email }}"><i class="envelope icon"></i></a>
22
+ <a class="ui circular twitter basic button" href="https://www.twitter.com/{{ site.twitter_username }}"><i class="twitter icon"></i></a>
23
+ <a class="ui circular github basic button" href="https://www.github.com/{{ site.github_username }}"><i class="github icon"></i></a>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <div class="ui center aligned container">
28
+ <div class="ui centered mini">
29
+ <p>
30
+ powered by <a title="Jekyll is a simple, blog-aware, static site generator." href="http://jekyllrb.com/">jekyll</a> &
31
+ <a title="Alex Recker's custom jekyll plugin" href="{{ site.baseurl }}{% link README.org %}">jekyll-recker</a> v{% recker_version %}
32
+ </p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </body>
37
+ </html>
@@ -0,0 +1,21 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+ <div class="ui container">
5
+ {%- include nav.html slug="index.html" title=site.title subtitle=site.description %}
6
+ <div class="ui text container center aligned">
7
+ {{ content }}
8
+ </div>
9
+ <br/>
10
+ <div class="ui container center aligned segment">
11
+ {% include stats.html %}
12
+ </div>
13
+ <br/>
14
+ <div class="ui container">
15
+ <img class="ui fluid image" src="{{ site.baseurl }}assets/images/words.png">
16
+ </div>
17
+ <br/>
18
+ <div class="ui container">
19
+ {% include archive.html %}
20
+ </div>
21
+ </div>
@@ -0,0 +1,10 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+ <div class="ui container">
5
+ {%- include nav.html slug=page.slug title=page.title subtitle=page.description %}
6
+ <div class="ui container">
7
+ {{ content }}
8
+ </div>
9
+ </div>
10
+
@@ -0,0 +1,12 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+ <div class="ui container">
5
+ {% capture datestring %}{{ page.date | date: '%A, %B %d %Y' }}{% endcapture %}
6
+ {%- include nav.html slug=page.slug title=datestring subtitle=page.title %}
7
+ <div class="ui container">
8
+ {{ content }}
9
+ {%- include pager.html %}
10
+ </div>
11
+ </div>
12
+
@@ -0,0 +1,15 @@
1
+ .ui
2
+ .container
3
+ margin: 2em
4
+
5
+ .footer
6
+ margin-top: 50px !important
7
+
8
+ p
9
+ font-size: 20px
10
+ line-height: 160%
11
+ -moz-osx-font-smoothing: grayscale
12
+ -webkit-font-smoothing: antialiased !important
13
+ -moz-font-smoothing: antialiased !important
14
+ text-rendering: optimizelegibility !important
15
+ letter-spacing: .03em
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ ---
2
+ ---
3
+ @import "{{ site.theme }}"
data/lib/blog/cli.rb ADDED
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commander'
4
+ require 'fileutils'
5
+
6
+ module Blog
7
+ # CLI
8
+ class CLI
9
+ include Commander::Methods
10
+
11
+ def logger
12
+ Blog::Log.logger
13
+ end
14
+
15
+ def config_path
16
+ @config_path ||= File.expand_path '~/.blog.yml'
17
+ end
18
+
19
+ def config
20
+ Blog::Config.load_from_file(config_path)
21
+ end
22
+
23
+ def journal
24
+ @journal ||= Blog::Journal.from_file(config.journal_path)
25
+ end
26
+
27
+ def latest
28
+ @latest ||= journal.public_entries.first
29
+ end
30
+
31
+ def build
32
+ logger.info "deleting #{config.site_dir.pretty_path}"
33
+ FileUtils.rm_rf(config.site_dir)
34
+ logger.info "parsing #{config.journal_path.pretty_path}"
35
+ journal = Blog::Journal.from_file(config.journal_path)
36
+ logger.info "writing #{journal.public_entries.count.pretty} public entries"
37
+ journal.write_public_entries! config.posts_dir
38
+ logger.info "building jekyll"
39
+ Blog::Jekyll.build(config)
40
+ end
41
+
42
+ def commit
43
+ git = Blog::Git.new(config.blog_repo)
44
+ git.run!
45
+ end
46
+
47
+ def slack
48
+ logger.info "fetched latest entry: #{latest.excerpt}"
49
+ config.slacks.each do |info|
50
+ Blog::Slacky.post(latest, `#{info['webhook_cmd']}`, info)
51
+ end
52
+ end
53
+
54
+ def run
55
+ program :name, 'blog'
56
+ program :version, 'v0.0.0'
57
+ program :description, 'script to generate and publish my blog'
58
+
59
+ default_command :all
60
+
61
+ global_option '--config FILE', String, 'path to blog.yml' do |file|
62
+ @config_path = file
63
+ end
64
+
65
+ command :build do |c|
66
+ c.syntax = 'build'
67
+ c.description = 'build jekyll site'
68
+ c.action do |_args, _options|
69
+ build
70
+ end
71
+ end
72
+
73
+ command :commit do |c|
74
+ c.syntax = 'commit'
75
+ c.description = 'commit and push new post'
76
+ c.action do |_args, _options|
77
+ commit
78
+ end
79
+ end
80
+
81
+ command :slack do |c|
82
+ c.syntax = 'slack'
83
+ c.description = 'send slack notifications'
84
+ c.action do |_args, _options|
85
+ slack
86
+ end
87
+ end
88
+
89
+ command :all do |c|
90
+ c.syntax = 'all'
91
+ c.description = 'build, commit, and slack'
92
+ c.action do |_args, _options|
93
+ build
94
+ commit
95
+ slack
96
+ end
97
+ end
98
+
99
+ run!
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ require 'yaml'
5
+
6
+ module Blog
7
+ # Config
8
+ class Config
9
+ attr_reader :data
10
+
11
+ def self.load_from_file(config_path = File.expand_path('~/.blog.yml'))
12
+ new(YAML.load_file(config_path) || {})
13
+ end
14
+
15
+ def initialize(data)
16
+ @data = data
17
+ end
18
+
19
+ def journal_path
20
+ File.join blog_repo, 'journal.org'
21
+ end
22
+
23
+ def posts_dir
24
+ File.join blog_repo, '_posts'
25
+ end
26
+
27
+ def site_dir
28
+ File.join(File.expand_path(blog_repo), '_site')
29
+ end
30
+
31
+ def blog_repo
32
+ Bundler.root.to_s
33
+ end
34
+
35
+ def stats_path
36
+ File.join blog_repo, '_data/stats.json'
37
+ end
38
+
39
+ def log_level
40
+ @data.fetch('log_level', 'INFO').upcase
41
+ end
42
+
43
+ def twitter_creds
44
+ twitter = @data.fetch('twitter')
45
+ creds = {}
46
+ [
47
+ 'access_token_secret',
48
+ 'access_token',
49
+ 'consumer_api_key',
50
+ 'consumer_api_secret'
51
+ ].each do |key|
52
+ creds[key] = `#{twitter.fetch(key + '_cmd')}`.strip
53
+ end
54
+ creds
55
+ end
56
+
57
+ def slacks
58
+ @data.fetch('slacks', [])
59
+ end
60
+
61
+ private
62
+
63
+ def missing_fields
64
+ required_keys.reject { |k| @data.key? k }
65
+ end
66
+ end
67
+ end
data/lib/blog/entry.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'org-ruby'
5
+
6
+ module Blog
7
+ # Entry
8
+ class Entry
9
+ def initialize(headline)
10
+ @headline = headline
11
+ end
12
+
13
+ def title
14
+ date.strftime('%A, %B %-e %Y')
15
+ end
16
+
17
+ def subtitle
18
+ @subtitle ||= @headline.headline_text.split(' ').drop(2).join(' ')
19
+ end
20
+
21
+ alias excerpt subtitle
22
+
23
+ def tags
24
+ @tags ||= @headline.tags
25
+ end
26
+
27
+ def date
28
+ @date ||= Date.strptime(
29
+ @headline.headline_text.split(' ').take(2).join(' '),
30
+ '%Y-%m-%d %A'
31
+ )
32
+ end
33
+
34
+ def date_slug
35
+ date.strftime('%Y-%m-%d')
36
+ end
37
+
38
+ def public?
39
+ !tags.include? 'private'
40
+ end
41
+
42
+ def filename
43
+ "#{date_slug}-#{date_slug}.html.html"
44
+ end
45
+
46
+ def permalink
47
+ "https://www.alexrecker.com/#{date_slug}.html"
48
+ end
49
+
50
+ def body_text
51
+ @headline.body_lines.drop(1).collect(&:output_text).join(' ')
52
+ end
53
+
54
+ def body_html
55
+ Orgmode::Parser.new(body_text).to_html
56
+ end
57
+
58
+ def to_html
59
+ <<~HTML
60
+ ---
61
+ title: #{title}
62
+ excerpt: #{excerpt}
63
+ ---
64
+ #{body_html}
65
+ HTML
66
+ end
67
+ end
68
+ end
data/lib/blog/git.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git'
4
+
5
+ module Blog
6
+ # Git
7
+ class Git
8
+ attr_reader :client
9
+
10
+ def initialize(path)
11
+ @client = ::Git.open(path)
12
+ @logger = Blog::Log.logger
13
+ end
14
+
15
+ def run!
16
+ commit!
17
+ end
18
+
19
+ def commit!
20
+ commit = '[auto] Automatic Publish'
21
+ @logger.info "writing commit: #{commit}"
22
+ @client.add
23
+ @client.commit(commit)
24
+ @logger.info 'pushing commit'
25
+ @client.push
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jekyll'
4
+
5
+ module Blog
6
+ module Jekyll
7
+ def self.build(config)
8
+ conf = ::Jekyll.configuration(
9
+ {
10
+ 'source' => config.blog_repo,
11
+ 'destination' => config.site_dir
12
+ }
13
+ )
14
+ ::Jekyll::Site.new(conf).process
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'org-ruby'
4
+
5
+ module Blog
6
+ # Journal
7
+ class Journal
8
+ def self.from_file(path)
9
+ new(Orgmode::Parser.load(path))
10
+ end
11
+
12
+ def logger
13
+ Blog::Log.logger
14
+ end
15
+
16
+ def initialize(parser)
17
+ @parser = parser
18
+ end
19
+
20
+ def public_entries
21
+ @public_entries ||= all_entries.select(&:public?).sort_by(&:date).reverse
22
+ end
23
+
24
+ def private_entries
25
+ @private_entries ||= all_entries.reject(&:public?).sort_by(&:date).reverse
26
+ end
27
+
28
+ def write_public_entries!(dir)
29
+ public_entries.each do |entry|
30
+ target = File.join(dir, entry.filename)
31
+ logger.debug "writing #{entry.title} to #{target.pretty_path}"
32
+ File.open(target, 'w+') { |f| f.write(entry.to_html) }
33
+ end
34
+ end
35
+
36
+ def all_entries
37
+ entry_headlines.map { |h| Entry.new(h) }
38
+ end
39
+
40
+ private
41
+
42
+ def entry_headlines
43
+ @parser.headlines.select { |h| h.level == 3 }
44
+ end
45
+ end
46
+ end
data/lib/blog/log.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Blog
6
+ # Log
7
+ module Log
8
+ def self.logger
9
+ @logger ||= default_logger
10
+ end
11
+
12
+ def self.default_logger
13
+ logger = Logger.new(STDOUT)
14
+ logger.level = Logger::INFO
15
+ logger.formatter = proc do |severity, _datetime, _progname, msg|
16
+ "blog: #{msg}\n"
17
+ end
18
+ logger
19
+ end
20
+
21
+ def self.level=(setting)
22
+ @logger.level = case setting.upcase
23
+ when 'DEBUG'
24
+ Logger::DEBUG
25
+ else
26
+ Logger::INFO
27
+ end
28
+ end
29
+ end
30
+ end