jekyll-github-pages-gem 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1e96acc9371c59ba0163040fc42595a54ec2dc7602b9d128313cb1b05d2c4106
4
+ data.tar.gz: 5310d22171f0150137e9abdbac7c0d3147f47f27f84f1c3c240c664e0033baef
5
+ SHA512:
6
+ metadata.gz: 07c6be16e280cd410f7a0f5c25e16ea7fdbd69611a2a7fb61ec19846428b4ca2fe53a48eb0aec9504b8532b17bb6de815bbde38fc0ce82c6a71e83f0e47881c3
7
+ data.tar.gz: d9cdef19e6748af33fcd01c8dd1a30fb6a032b38839b924d168b26789720e63aeb032002e7cdbc699fb55f2bf58b45ea13fb6ca7a0c395e4c368a245da4743e8
data/Gemfile ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ gem 'kramdown'
7
+
8
+ gem 'octokit', '~> 4.18'
9
+
10
+ gem 'rubocop', '~> 0.71'
11
+
12
+ gem 'carrierwave', '>= 2.0.0.rc', '< 3.0'
13
+
14
+ # Octokit does not work with the most recent version of faraday so this locks it to a version that works.
15
+ gem 'faraday', '~> 0.17.1'
16
+
17
+ gem 'rake'
18
+
19
+ group :test do
20
+ gem 'minitest'
21
+ gem 'mocha'
22
+ gem 'simplecov'
23
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,97 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jekyll-github-pages-gem (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activemodel (6.0.2.2)
10
+ activesupport (= 6.0.2.2)
11
+ activesupport (6.0.2.2)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 0.7, < 2)
14
+ minitest (~> 5.1)
15
+ tzinfo (~> 1.1)
16
+ zeitwerk (~> 2.2)
17
+ addressable (2.7.0)
18
+ public_suffix (>= 2.0.2, < 5.0)
19
+ ast (2.4.0)
20
+ carrierwave (2.1.0)
21
+ activemodel (>= 5.0.0)
22
+ activesupport (>= 5.0.0)
23
+ addressable (~> 2.6)
24
+ image_processing (~> 1.1)
25
+ mimemagic (>= 0.3.0)
26
+ mini_mime (>= 0.1.3)
27
+ concurrent-ruby (1.1.6)
28
+ docile (1.3.2)
29
+ faraday (0.17.3)
30
+ multipart-post (>= 1.2, < 3)
31
+ ffi (1.12.2)
32
+ ffi (1.12.2-x64-mingw32)
33
+ i18n (1.8.2)
34
+ concurrent-ruby (~> 1.0)
35
+ image_processing (1.10.3)
36
+ mini_magick (>= 4.9.5, < 5)
37
+ ruby-vips (>= 2.0.17, < 3)
38
+ jaro_winkler (1.5.4)
39
+ kramdown (2.1.0)
40
+ mimemagic (0.3.4)
41
+ mini_magick (4.10.1)
42
+ mini_mime (1.0.2)
43
+ minitest (5.14.0)
44
+ mocha (1.11.2)
45
+ multipart-post (2.1.1)
46
+ octokit (4.18.0)
47
+ faraday (>= 0.9)
48
+ sawyer (~> 0.8.0, >= 0.5.3)
49
+ parallel (1.19.1)
50
+ parser (2.7.1.0)
51
+ ast (~> 2.4.0)
52
+ public_suffix (4.0.4)
53
+ rainbow (3.0.0)
54
+ rake (13.0.1)
55
+ rexml (3.2.4)
56
+ rubocop (0.81.0)
57
+ jaro_winkler (~> 1.5.1)
58
+ parallel (~> 1.10)
59
+ parser (>= 2.7.0.1)
60
+ rainbow (>= 2.2.2, < 4.0)
61
+ rexml
62
+ ruby-progressbar (~> 1.7)
63
+ unicode-display_width (>= 1.4.0, < 2.0)
64
+ ruby-progressbar (1.10.1)
65
+ ruby-vips (2.0.17)
66
+ ffi (~> 1.9)
67
+ sawyer (0.8.2)
68
+ addressable (>= 2.3.5)
69
+ faraday (> 0.8, < 2.0)
70
+ simplecov (0.18.5)
71
+ docile (~> 1.1)
72
+ simplecov-html (~> 0.11)
73
+ simplecov-html (0.12.2)
74
+ thread_safe (0.3.6)
75
+ tzinfo (1.2.7)
76
+ thread_safe (~> 0.1)
77
+ unicode-display_width (1.7.0)
78
+ zeitwerk (2.3.0)
79
+
80
+ PLATFORMS
81
+ ruby
82
+ x64-mingw32
83
+
84
+ DEPENDENCIES
85
+ carrierwave (>= 2.0.0.rc, < 3.0)
86
+ faraday (~> 0.17.1)
87
+ jekyll-github-pages-gem!
88
+ kramdown
89
+ minitest
90
+ mocha
91
+ octokit (~> 4.18)
92
+ rake
93
+ rubocop (~> 0.71)
94
+ simplecov
95
+
96
+ BUNDLED WITH
97
+ 2.1.4
data/README.md ADDED
@@ -0,0 +1,16 @@
1
+ [![Build Status](https://travis-ci.org/msoe-sg/jekyll-github-pages-gem.svg?branch=master)](https://travis-ci.org/msoe-sg/jekyll-github-pages-gem)
2
+
3
+ ## Setup
4
+ 1. Follow the instructions from the wiki article [here](https://github.com/msoe-sg/msoe-sg-website/wiki/Environment-Setup) to setup your development environment.
5
+ 2. Open up a terminal to the folder where you want to clone the repo and run the command `git@github.com:msoe-sg/jekyll-github-pages-gem.git`
6
+ 3. After run the clone change into the project directory by running the command `cd jekyll-github-pages-gem`
7
+ 4. Next install the dependencies for the project by running the command `bundle install`
8
+ 5. Contribute
9
+ Our git flow process is typical--we have a master branch that gets released to the public, and feature branches for individual tasks. We don't have a development branch yet since this isn't used in production yet.
10
+ If you have questions on how to contribute, please contact admin@msoe-sse.com or msoe.sg.hosting@gmail.com and we will get back to you at our earliest convenience.
11
+
12
+ ## Continuous Integration
13
+ There are checks that will be performed whenever Pull Requests are opened. To save time on the build server, please run the tests locally to check for errors that will occur in the CI builds.
14
+
15
+ 1. To run [Rubocop](https://github.com/ashmaroli/rubocop-jekyll), run the command `bundle exec rubocop`
16
+ 2. To run all unit tests, run the command `rake`
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+
5
+ task default: 'test'
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.test_files = FileList['tests/**/*_test.rb']
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'jekyll-github-pages-gem'
5
+ s.version = '1.0.0'
6
+ s.summary = 'A gem that uses the github API to make edits with a jekyll blog'
7
+ s.files = Dir['*', 'lib/**/*']
8
+ s.require_paths = ['lib']
9
+ s.licenses = ['MIT']
10
+ s.authors = ['MSOE SSE Web Team']
11
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/post'
4
+
5
+ module Factories
6
+ ##
7
+ # This class is a factory for parsing post text and creating a correseponding post model
8
+ class PostFactory
9
+ LEAD = '{: .lead}'
10
+ BREAK = '<!–-break-–>'
11
+
12
+ # serves as the default hero for a post if none is provided.
13
+ DEFAULT_HERO = 'https://source.unsplash.com/collection/145103/'
14
+
15
+ ##
16
+ # This method parses markdown in a post a returns a post model
17
+ #
18
+ # Params:
19
+ # +post_contents+::markdown in a given post
20
+ # +file_path+::the path on GitHub to the post
21
+ # +ref+::a sha for a ref indicating the head of a branch a post is pushed to on the GitHub server
22
+ def create_post(post_contents, file_path, ref)
23
+ create_post_model(post_contents, file_path, ref) if !post_contents.nil? && post_contents.is_a?(String)
24
+ end
25
+
26
+ private
27
+
28
+ def parse_tags(header)
29
+ result = []
30
+ header.lines.each do |line|
31
+ tag_match = line.match(/\s*-\s*(.*)/)
32
+ result << tag_match.captures.first if tag_match
33
+ end
34
+ result.join(', ')
35
+ end
36
+
37
+ def create_post_model(post_contents, file_path, ref)
38
+ result = Post.new
39
+
40
+ result.file_path = file_path
41
+ result.github_ref = ref
42
+
43
+ # What this regular expression does is it matches three groups
44
+ # The first group represents the header of the post which appears
45
+ # between the two --- lines. The second group is for helping capture newline characters
46
+ # correctly and the third group is the actual post contents
47
+ match_obj = post_contents.match(/---(.*)---(\r\n|\r|\n)(.*)/m)
48
+ header = match_obj.captures[0]
49
+
50
+ parse_post_header(header, result)
51
+ result.contents = match_obj.captures[2]
52
+ .remove("#{LEAD}\r\n")
53
+ .remove("#{LEAD}\n")
54
+ .remove("#{BREAK}\r\n")
55
+ .remove("#{BREAK}\n")
56
+ result.tags = parse_tags(header)
57
+ result
58
+ end
59
+
60
+ def parse_post_header(header, post_model)
61
+ # The following regular expressions in this method look for specific properities
62
+ # located in the post header.
63
+ post_model.title = header.match(/title:\s*(.*)(\r\n|\r|\n)/).captures.first
64
+ post_model.author = header.match(/author:\s*(.*)(\r\n|\r|\n)/).captures.first
65
+ post_model.hero = header.match(/hero:\s*(.*)(\r\n|\r|\n)/).captures.first
66
+ post_model.hero = '' if post_model.hero == DEFAULT_HERO
67
+ post_model.overlay = header.match(/overlay:\s*(.*)(\r\n|\r|\n)/).captures.first
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Third Party Dependencies
4
+ require 'carrierwave'
5
+
6
+ # Source Files
7
+ require 'factories/post_factory'
8
+ require 'services/post_services/base_post_service'
9
+ require 'services/post_services/post_creation_service'
10
+ require 'services/post_services/post_editing_service'
11
+ require 'services/post_services/post_pull_request_editing_service'
12
+ require 'services/github_service'
13
+
14
+ require 'services/kramdown_service'
15
+
16
+ require 'models/post'
17
+ require 'models/post_image_manager'
18
+
19
+ require 'uploaders/post_image_uploader'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # An object representing an image that is on a post
5
+ class PostImage
6
+ attr_accessor :filename
7
+ # The binary contents of an image as a Base64 string
8
+ attr_accessor :contents
9
+ end
10
+
11
+ ##
12
+ # An object representing a post on the Jekyll website
13
+ class Post
14
+ attr_accessor :title
15
+ attr_accessor :author
16
+ attr_accessor :hero
17
+ attr_accessor :overlay
18
+ attr_accessor :contents
19
+ attr_accessor :tags
20
+ # Path to the markdown post starting at the root of the repository
21
+ attr_accessor :file_path
22
+ # The GitHub ref the post's markdown is at. This is used to indicate
23
+ # whether a post is in PR or not
24
+ attr_accessor :github_ref
25
+ attr_accessor :images
26
+
27
+ def initialize
28
+ @images = []
29
+ end
30
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ ##
6
+ # A singleton class for managing all image attachments for a post
7
+ class PostImageManager
8
+ include Singleton
9
+
10
+ attr_reader :uploaders
11
+ attr_reader :downloaded_images
12
+ attr_accessor :root_dir
13
+
14
+ ##
15
+ # The constructor for PostImageManager which initializes the array of Carrierware
16
+ # image uploaders to use when submiting a post and the array of downloaded images
17
+ def initialize
18
+ @uploaders = []
19
+ @downloaded_images = []
20
+ @root_dir = ''
21
+ end
22
+
23
+ ##
24
+ # Adds an image to be uploaded in a Jekyll website post
25
+ #
26
+ # Params:
27
+ # +file+:: A ActionDispatch::Http::UploadedFile object containing the file to be used in a post
28
+ def add_file(file)
29
+ uploader_to_add = PostImageUploader.new
30
+ uploader_to_add.cache!(file)
31
+ @uploaders.delete_if { |x| x.filename == file.original_filename }
32
+ @uploaders << uploader_to_add
33
+ end
34
+
35
+ ##
36
+ # Adds an image that was downloaded from the Jekyll website repo
37
+ #
38
+ # Params:
39
+ # +downloaded_image+:: A PostImage object representing the downloaded image
40
+ def add_downloaded_image(downloaded_image)
41
+ @downloaded_images << downloaded_image
42
+ end
43
+
44
+ ##
45
+ # Clears the manager of all currently exisiting image uploaders and delete's their cache directories.
46
+ # Also clears the manager of all of the downloaded images
47
+ def clear(_root_dir)
48
+ @uploaders.each do |uploader|
49
+ full_preview_path = "#{@root_dir}/public/uploads/tmp/#{uploader.preview.cache_name}"
50
+ cache_dir = File.expand_path('..', full_preview_path)
51
+ uploader.remove!
52
+ Dir.delete(cache_dir)
53
+ end
54
+
55
+ @uploaders.clear
56
+ @downloaded_images.clear
57
+ end
58
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'octokit'
4
+ require 'base64'
5
+ require 'date'
6
+ require 'cgi'
7
+ require_relative 'kramdown_service'
8
+ require_relative '../factories/post_factory'
9
+
10
+ module Services
11
+ ##
12
+ # This class contains all operations involving interacting with the GitHub API
13
+ class GithubService
14
+ def initialize(full_repo_name, access_token)
15
+ @full_repo_name = full_repo_name
16
+ @access_token = access_token
17
+
18
+ @kramdown_service = Services::KramdownService.new
19
+ @post_factory = Factories::PostFactory.new
20
+ end
21
+
22
+ ##
23
+ # This method fetches all the markdown contents of all the posts on a Jekyll website
24
+ # that have been written and returns a list of models representing a Post.
25
+ def get_all_posts
26
+ result = []
27
+ client = create_octokit_client
28
+ posts = client.contents(@full_repo_name, path: '_posts')
29
+ posts.each do |post|
30
+ post_api_response = client.contents(@full_repo_name, path: post.path)
31
+
32
+ post_model = create_post_from_api_response(post_api_response, nil)
33
+ image_paths = @kramdown_service.get_all_image_paths(post_model.contents)
34
+
35
+ images = []
36
+ image_paths.each do |image_path|
37
+ image_content = client.contents(@full_repo_name, path: image_path)
38
+ images << create_post_image(image_path, image_content.content)
39
+ end
40
+
41
+ post_model.images = images
42
+
43
+ result << post_model
44
+ end
45
+ result
46
+ end
47
+
48
+ ##
49
+ # This method fetches all of the posts that have been written but have not been merged into master yet.
50
+ def get_all_posts_in_pr(pr_body)
51
+ result = []
52
+ client = create_octokit_client
53
+ pull_requests_for_user = get_open_jekyll_pull_requests(pr_body)
54
+
55
+ pull_requests_for_user.each do |pull_request|
56
+ pull_request_files = client.pull_request_files(@full_repo_name, pull_request[:number])
57
+
58
+ post = nil
59
+ images = []
60
+ pull_request_files.each do |pull_request_file|
61
+ contents_url_params = CGI.parse(pull_request_file[:contents_url])
62
+
63
+ # The CGI.parse method returns a hash with the key being the URL and the value being an array of
64
+ # URI parameters so in order to get the ref we need to grab the first value in the hash and the first
65
+ # URI parameter in the first hash value
66
+ ref = contents_url_params.values.first.first
67
+ file_contents = client.contents(@full_repo_name, path: pull_request_file[:filename], ref: ref)
68
+
69
+ if pull_request_file[:filename].end_with?('.md')
70
+ post = create_post_from_api_response(file_contents, ref)
71
+ result << post
72
+ else
73
+ images << create_post_image(pull_request_file[:filename], file_contents.content)
74
+ end
75
+ end
76
+
77
+ post.images = images
78
+ end
79
+ result
80
+ end
81
+
82
+ ##
83
+ # This method fetches a single post from a Jekyll website given a post title
84
+ # and returns a Post model
85
+ #
86
+ # Params:
87
+ # +title+:: A title of a Jekyll website post
88
+ # +ref+::a sha for a ref indicating the head of a branch a post is pushed to on the GitHub server
89
+ def get_post_by_title(title, ref)
90
+ result = nil
91
+ result = get_all_posts_in_pr.find { |x| x.title == title } if ref
92
+ result = get_all_posts.find { |x| x.title == title } unless ref
93
+ result&.images&.each { |x| PostImageManager.instance.add_downloaded_image(x) }
94
+ result
95
+ end
96
+
97
+ ##
98
+ # This method gets the sha of the commit at the head of master in a Jekyll website repo
99
+ def get_master_head_sha
100
+ client = create_octokit_client
101
+ client.ref(@full_repo_name, 'heads/master')[:object][:sha]
102
+ end
103
+
104
+ ##
105
+ # This method gets the sha of the base tree for a given branch in a Jekyll website repo
106
+ #
107
+ # Params
108
+ # +head_sha+::the sha of the head of a certain branch
109
+ def get_base_tree_for_branch(head_sha)
110
+ client = create_octokit_client
111
+ client.commit(@full_repo_name, head_sha)[:commit][:tree][:sha]
112
+ end
113
+
114
+ ##
115
+ # This method create a new blob in a Jekyll website repo with text content
116
+ #
117
+ # Params
118
+ # +text+::the text content to create a blob for
119
+ def create_text_blob(text)
120
+ client = create_octokit_client
121
+ client.create_blob(@full_repo_name, text)
122
+ end
123
+
124
+ ##
125
+ # This method creates a new blob in a Jekyll website with base 64 encoded content
126
+ #
127
+ # Params
128
+ # +content+::the base 64 encoded content to create a blob for
129
+ def create_base64_encoded_blob(content)
130
+ client = create_octokit_client
131
+ client.create_blob(@full_repo_name, content, 'base64')
132
+ end
133
+
134
+ ##
135
+ # This method creates a new tree in a Jekyll website repo and returns the tree's sha.
136
+ # The method assumes that the paths passed into the method have corresponding blobs
137
+ # created for the files
138
+ #
139
+ # Params:
140
+ # +file_information+::an array of hashes containing the file path and the blob sha for a file
141
+ # +sha_base_tree+::the sha of the base tree
142
+ def create_new_tree_with_blobs(file_information, sha_base_tree)
143
+ client = create_octokit_client
144
+ blob_information = []
145
+ file_information.each do |file|
146
+ # This mode property on this hash represents the file mode for a GitHub tree.
147
+ # The mode is 100644 for a file blob. See https://developer.github.com/v3/git/trees/ for more information
148
+ blob_information << { path: file[:path],
149
+ mode: '100644',
150
+ type: 'blob',
151
+ sha: file[:blob_sha] }
152
+ end
153
+ client.create_tree(@full_repo_name, blob_information, base_tree: sha_base_tree)[:sha]
154
+ end
155
+
156
+ ##
157
+ # This method commits and pushes a tree to a Jekyll website repo
158
+ #
159
+ # Params:
160
+ # +commit_message+::the message for the new commit
161
+ # +tree_sha+::the sha of the tree to commit
162
+ # +head_sha+::the sha of the head to commit from
163
+ def commit_and_push_to_repo(commit_message, tree_sha, head_sha, ref_name)
164
+ client = create_octokit_client
165
+ sha_new_commit = client.create_commit(@full_repo_name, commit_message, tree_sha, head_sha)[:sha]
166
+ client.update_ref(@full_repo_name, ref_name, sha_new_commit)
167
+ end
168
+
169
+ ##
170
+ # This method creates a pull request for a branch in a Jekyll website repo
171
+ #
172
+ # Params:
173
+ # +source_branch+::the source branch for the PR
174
+ # +base_branch+::the base branch for the PR
175
+ # +pr_title+::the title for the PR
176
+ # +pr_body+::the body for the PR
177
+ # +reviewers+::an array of pull request reviewers for the PR
178
+ def create_pull_request(source_branch, base_branch, pr_title, pr_body, reviewers)
179
+ client = create_octokit_client
180
+ pull_number = client.create_pull_request(@full_repo_name, base_branch, source_branch, pr_title, pr_body)[:number]
181
+ client.request_pull_request_review(@full_repo_name, pull_number, reviewers: reviewers)
182
+ end
183
+
184
+ ##
185
+ # This method will create a branch in a Jekyll website repo
186
+ # if it already doesn't exist
187
+ #
188
+ # Params:
189
+ # +ref_name+:: the name of the branch to create if necessary
190
+ # +master_head_sha+:: the sha representing the head of master
191
+ def create_ref_if_necessary(ref_name, master_head_sha)
192
+ client = create_octokit_client
193
+ client.ref(@full_repo_name, ref_name)
194
+ rescue Octokit::NotFound
195
+ client.create_ref(@full_repo_name, ref_name, master_head_sha)
196
+ end
197
+
198
+ ##
199
+ # This method will fetch a GitHub's ref name given it's sha identifier.
200
+ # It will also strip off the starting refs portion of the name
201
+ #
202
+ # Params:
203
+ # +oauth_token+::a user's oauth access token
204
+ # +ref_sha+:: the sha of the ref to fetch
205
+ def get_ref_name_by_sha(ref_sha)
206
+ client = create_octokit_client
207
+ ref_response = client.refs(@full_repo_name).find { |x| x[:object][:sha] == ref_sha }
208
+ ref_response[:ref].match(%r{refs/(.*)}).captures.first
209
+ end
210
+
211
+ private
212
+
213
+ def create_post_from_api_response(post, ref)
214
+ # Base64.decode64 will convert our string into a ASCII string
215
+ # calling force_encoding('UTF-8') will fix that problem
216
+ text_contents = Base64.decode64(post.content).dup.force_encoding('UTF-8')
217
+ @post_factory.create_post(text_contents, post.path, ref)
218
+ end
219
+
220
+ def get_open_jekyll_pull_requests(pull_request_body)
221
+ client = create_octokit_client
222
+ open_pull_requests = client.pull_requests(@full_repo_name, state: 'open')
223
+ open_pull_requests.select { |x| x[:body] == pull_request_body }
224
+ end
225
+
226
+ def create_post_image(filename, contents)
227
+ result = PostImage.new
228
+ result.filename = filename
229
+ result.contents = contents
230
+ result
231
+ end
232
+
233
+ def create_octokit_client
234
+ Octokit::Client.new(access_token: @access_token)
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kramdown'
4
+
5
+ ##
6
+ # This modules contains extentions of the Kramdown::Convert module for custom kramdown converters
7
+ module Kramdown
8
+ module Converter
9
+ ##
10
+ # A custom kramdown HTML converter for getting the HTML preview for a post
11
+ class Preview < Html
12
+ ##
13
+ # An override of the convert_img tag which converts all image sources to pull
14
+ # from the CarrierWare cache location if an uploader exists with the image's filename.
15
+ # Or the Base64 contents of a downloaded image are replaced in the src attribute if the image
16
+ # was downloaded for the post
17
+ #
18
+ # Params:
19
+ # +el+::the image element to convert to html
20
+ # +indent+::the indent of the HTML
21
+ def convert_img(element, indent)
22
+ formatted_filename = File.basename(element.attr['src']).tr(' ', '_')
23
+ uploader = PostImageManager.instance.uploaders.find { |x| x.filename == formatted_filename }
24
+ if uploader
25
+ element.attr['src'] = "/uploads/tmp/#{uploader.preview.cache_name}"
26
+ else
27
+ downloaded_image = PostImageManager.instance.downloaded_images
28
+ .find { |x| File.basename(x.filename) == File.basename(element.attr['src']) }
29
+ if downloaded_image
30
+ extension = File.extname(downloaded_image.filename)
31
+ extension[0] = ''
32
+ element.attr['src'] = "data:image/#{extension};base64,#{downloaded_image.contents}"
33
+ end
34
+ end
35
+
36
+ super(element, indent)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ module Services
43
+ ##
44
+ # This class contains all operations with interacting with the kramdown engine
45
+ class KramdownService
46
+ DEFAULT_HERO = 'https://source.unsplash.com/collection/145103/'
47
+ ##
48
+ # This method takes given markdown and converts it to HTML for the post preview
49
+ #
50
+ # Params:
51
+ # +text+:: markdown to convert to html
52
+ def get_preview(text)
53
+ Kramdown::Document.new(text).to_preview
54
+ end
55
+
56
+ ##
57
+ # This method returns the image filename given some markdown
58
+ #
59
+ # Params:
60
+ # +image_file_name+:: a filename of a image to look for in markdown
61
+ # +markdown+:: text of a markdown post
62
+ def get_image_filename_from_markdown(image_file_name, markdown)
63
+ document = Kramdown::Document.new(markdown)
64
+ document_descendants = []
65
+
66
+ get_document_descendants(document.root, document_descendants)
67
+ all_img_tags = document_descendants.select { |x| x.type == :img }
68
+ matching_image_tag = all_img_tags.find { |x| get_filename_for_image_tag(x).tr(' ', '_') == image_file_name }
69
+
70
+ return get_filename_for_image_tag(matching_image_tag) if matching_image_tag
71
+
72
+ nil
73
+ end
74
+
75
+ ##
76
+ # This method returns an array of all image paths given some markdown
77
+ #
78
+ # Params:
79
+ # +markdown+:: text of a markdown post
80
+ def get_all_image_paths(markdown)
81
+ document = Kramdown::Document.new(markdown)
82
+ document_descendants = []
83
+
84
+ get_document_descendants(document.root, document_descendants)
85
+ all_img_tags = document_descendants.select { |x| x.type == :img }
86
+
87
+ result = all_img_tags.map do |img_tag|
88
+ img_tag.attr['src'][1..-1] if img_tag.attr['src'] !~ URI::DEFAULT_PARSER.make_regexp
89
+ end
90
+
91
+ result.compact
92
+ end
93
+
94
+ ##
95
+ # This method takes parameters for a given post and formats them
96
+ # as a valid jekyll post for a Jekyll website
97
+ #
98
+ # Params:
99
+ # +text+:: the markdown contents of the post
100
+ # +author+:: the author of the post
101
+ # +title+:: the title of the post
102
+ # +tags+:: tags specific to the post
103
+ # +overlay+:: the overlay color of the post
104
+ # +hero+:: a link to an optional background image for a post
105
+ def create_jekyll_post_text(text, author, title, tags, overlay, hero)
106
+ header_converted_text = fix_header_syntax(text)
107
+ header_converted_text = add_line_break_to_markdown_if_necessary(header_converted_text)
108
+
109
+ parsed_tags = parse_tags(tags)
110
+
111
+ tag_section = %(tags:
112
+ #{parsed_tags})
113
+
114
+ lead_break_section = "{: .lead}\r\n<!–-break-–>"
115
+
116
+ hero_to_use = hero
117
+ hero_to_use = DEFAULT_HERO if hero_to_use.empty?
118
+ result = %(---
119
+ layout: post
120
+ title: #{title}
121
+ author: #{author}\r\n)
122
+
123
+ result += "#{tag_section}\r\n" unless parsed_tags.empty?
124
+ result += %(hero: #{hero_to_use}
125
+ overlay: #{overlay.downcase}
126
+ published: true
127
+ ---
128
+ #{lead_break_section}
129
+ #{header_converted_text})
130
+
131
+ result
132
+ end
133
+
134
+ private
135
+
136
+ def parse_tags(tags)
137
+ tag_array = tags.split(',')
138
+ result = ''
139
+ tag_array.each do |tag|
140
+ result += " - #{tag.strip}"
141
+ result += "\r\n" if tag != tag_array.last
142
+ end
143
+ result
144
+ end
145
+
146
+ def fix_header_syntax(text)
147
+ document = Kramdown::Document.new(text)
148
+ header_elements = document.root.children.select { |x| x.type == :header }
149
+ lines = text.split("\n")
150
+ lines = lines.map do |line|
151
+ if header_elements.any? { |x| line.include? x.options[:raw_text] }
152
+ # This regex matches the line into 2 groups with the first group being the repeating #
153
+ # characters and the beginning of the string and the second group being the rest of the string
154
+ line_match = line.match(/(#*)(.*)/)
155
+ line = "#{line_match.captures.first} #{line_match.captures.last.strip}"
156
+ else
157
+ line.delete("\r\n")
158
+ end
159
+ end
160
+ lines.join("\r\n")
161
+ end
162
+
163
+ def get_document_descendants(current_element, result)
164
+ current_element.children.each do |element|
165
+ result << element
166
+ get_document_descendants(element, result)
167
+ end
168
+ end
169
+
170
+ def get_filename_for_image_tag(image_el)
171
+ File.basename(image_el.attr['src'])
172
+ end
173
+
174
+ def add_line_break_to_markdown_if_necessary(markdown)
175
+ lines = markdown.split("\n")
176
+ # The regular expression in the if statement looks for a markdown reference to a link like
177
+ # [logo]: https://ieeextreme.org/wp-content/uploads/2019/05/Xtreme_colour-e1557478323964.png
178
+ # If a post starts with that reference in jekyll followed by an image using that reference
179
+ # the line below will be interperted as a paragraph tag instead of an image tag. To fix that
180
+ # we add a line break to the start of the markdown.
181
+ return "\r\n#{markdown}" if lines.first&.match?(/\[(.*)\]: (.*)/)
182
+
183
+ markdown
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Services
4
+ ##
5
+ # The base class for service classes responsible for performing operations on posts
6
+ class BasePostService
7
+ def initialize(repo_name, access_token)
8
+ @github_service = GithubService.new(repo_name, access_token)
9
+ @kramdown_service = KramdownService.new
10
+ end
11
+
12
+ protected
13
+
14
+ def create_new_tree(post_markdown, post_title, post_file_path, sha_base_tree)
15
+ file_information = [create_blob_for_post(post_markdown, post_title, post_file_path)]
16
+ create_image_blobs(post_markdown, file_information)
17
+ @github_service.create_new_tree_with_blobs(file_information, sha_base_tree)
18
+ end
19
+
20
+ private
21
+
22
+ def create_blob_for_post(post_markdown, _post_title, post_file_path)
23
+ blob_sha = @github_service.create_text_blob(post_markdown)
24
+ { path: post_file_path, blob_sha: blob_sha }
25
+ end
26
+
27
+ def create_image_blobs(post_markdown, current_file_information)
28
+ PostImageManager.instance.uploaders.each do |uploader|
29
+ # This check prevents against images that have been removed from the markdown
30
+ markdown_file_name = @kramdown_service.get_image_filename_from_markdown(uploader.filename, post_markdown)
31
+ next unless markdown_file_name
32
+
33
+ # This line uses .file.file since the first .file returns a carrierware object
34
+ File.open(uploader.post_image.file.file, 'rb') do |file|
35
+ base_64_encoded_image = Base64.encode64(file.read)
36
+ image_blob_sha = @github_service.create_base64_encoded_blob(base_64_encoded_image)
37
+ current_file_information << { path: "assets/img/#{markdown_file_name}", blob_sha: image_blob_sha }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base_post_service'
4
+ require_relative '../github_service'
5
+
6
+ module Services
7
+ ##
8
+ # This class is responsible for creating posts on a Jekyll website
9
+ class PostCreationService < BasePostService
10
+ def initialize(repo_name, access_token)
11
+ super(repo_name, access_token)
12
+ end
13
+
14
+ ##
15
+ # This method submits a new post to GitHub by checking out a new branch for the post,
16
+ # if the branch already doesn't exist. Commiting and pushing the markdown and any photos
17
+ # attached to the post to the branch. And then finally opening a pull request into master
18
+ # for the new post.
19
+ #
20
+ # Params
21
+ # +oauth_token+::a user's oauth access token
22
+ # +post_markdown+:: the markdown contents of a post
23
+ # +pull_request_body+::an optional pull request body for the post, it will be blank if nothing is provided
24
+ # +reviewers+:: an optional list of reviewers for the post PR
25
+ def create_post(post_markdown, post_title, pull_request_body = '', reviewers = [])
26
+ # This ref_name variable represents the branch name
27
+ # for creating a post. At the end we strip out all of the whitespace in
28
+ # the post_title to create a valid branch name
29
+ branch_name = "createPost#{post_title.gsub(/\s+/, '')}"
30
+ ref_name = "heads/#{branch_name}"
31
+
32
+ master_head_sha = @github_service.get_master_head_sha
33
+ sha_base_tree = @github_service.get_base_tree_for_branch(master_head_sha)
34
+
35
+ @github_service.create_ref_if_necessary(ref_name, master_head_sha)
36
+
37
+ new_post_path = create_new_filepath_for_post(post_title)
38
+ new_tree_sha = create_new_tree(post_markdown, post_title, new_post_path, sha_base_tree)
39
+
40
+ @github_service.commit_and_push_to_repo("Created post #{post_title}",
41
+ new_tree_sha, master_head_sha, ref_name)
42
+ @github_service.create_pull_request(branch_name, 'master', "Created Post #{post_title}",
43
+ pull_request_body,
44
+ reviewers)
45
+
46
+ PostImageManager.instance.clear
47
+ end
48
+
49
+ private
50
+
51
+ def create_new_filepath_for_post(post_title)
52
+ "_posts/#{DateTime.now.strftime('%Y-%m-%d')}-#{post_title.gsub(/\s+/, '')}.md"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base_post_service'
4
+ require_relative '../github_service'
5
+
6
+ module Services
7
+ ##
8
+ # This class is responsible for editing posts on a Jekyll website
9
+ class PostEditingService < BasePostService
10
+ def initialize(repo_name, access_token)
11
+ super(repo_name, access_token)
12
+ end
13
+
14
+ ##
15
+ # This method submits changes to an existing post to GitHub by checking out a new branch for the post,
16
+ # if the branch already doesn't exist. Commiting and pushing the markdown changes and any added photos
17
+ # for the existing post to the branch. And the finally opening a pull request into master for the new post.
18
+ #
19
+ # Params
20
+ # +post_markdown+::the modified markdown to submit
21
+ # +post_title+::the title for the existing post
22
+ # +existing_post_file_path+::the file path to the existing post on GitHub
23
+ # +pull_request_body+::an optional pull request body for the post, it will be blank if nothing is provided
24
+ # +reviewers+:: an optional list of reviewers for the post PR
25
+ def edit_post(post_markdown, post_title, existing_post_file_path, pull_request_body = '', reviewers = [])
26
+ # This ref_name variable represents the branch name
27
+ # for editing a post. At the end we strip out all of the whitespace in
28
+ # the post_title to create a valid branch name
29
+ branch_name = "editPost#{post_title.gsub(/\s+/, '')}"
30
+ ref_name = "heads/#{branch_name}"
31
+
32
+ master_head_sha = @github_service.get_master_head_sha
33
+ sha_base_tree = @github_service.get_base_tree_for_branch(master_head_sha)
34
+
35
+ @github_service.create_ref_if_necessary(ref_name, master_head_sha)
36
+ new_tree_sha = create_new_tree(post_markdown, post_title, existing_post_file_path, sha_base_tree)
37
+
38
+ @github_service.commit_and_push_to_repo("Edited post #{post_title}", new_tree_sha, master_head_sha, ref_name)
39
+ @github_service.create_pull_request(branch_name, 'master', "Edited Post #{post_title}",
40
+ pull_request_body,
41
+ reviewers)
42
+
43
+ PostImageManager.instance.clear
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './base_post_service'
4
+ require_relative '../github_service'
5
+
6
+ module Services
7
+ ##
8
+ # This class is responsible for editing posts that are in PR on a Jekyll website
9
+ class PostPullRequestEditingService < BasePostService
10
+ def initialize(repo_name, access_token)
11
+ super(repo_name, access_token)
12
+ end
13
+
14
+ ##
15
+ # This method submits changes to a post that is already in PR, commiting and pushing the markdown changes
16
+ # and any added photos to the branch. Since the post is in PR these changes will be a PR updated to the given branch
17
+ #
18
+ # Params:
19
+ # +post_markdown+::the modified markdown to submit
20
+ # +post_title+::the title for the existing post
21
+ # +existing_post_file_path+::the file path to the existing post on GitHub
22
+ # +ref+::the ref to update
23
+ def edit_post_in_pr(post_markdown, post_title, existing_post_file_path, ref)
24
+ ref_name = @github_service.get_ref_name_by_sha(ref)
25
+ sha_base_tree = @github_service.get_base_tree_for_branch(ref)
26
+
27
+ new_tree_sha = create_new_tree(post_markdown, post_title, existing_post_file_path, sha_base_tree)
28
+ @github_service.commit_and_push_to_repo("Edited post #{post_title}", new_tree_sha, ref, ref_name)
29
+
30
+ PostImageManager.instance.clear
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'carrierwave'
4
+ ##
5
+ # The file uploader class for uploading images to a Jekyll website post
6
+ class PostImageUploader < CarrierWave::Uploader::Base
7
+ include CarrierWave::MiniMagick
8
+ # These constants represent the maximum width and height an uploaded can be for the post preview
9
+ # and for actually appearing on a Jekyll website. These numbers were initially determined by testing
10
+ # with a 1920x1080 image. If you find a reason to change these numbers please document the reason
11
+ # below
12
+ PREVIEW_LIMIT = [800, 800].freeze
13
+ POST_LIMIT = [800, 700].freeze
14
+
15
+ storage :file
16
+
17
+ ##
18
+ # Limits only images to be uploaded to an SSE website post
19
+ def extension_whitelist
20
+ %w[jpg jpeg gif png]
21
+ end
22
+
23
+ def size_range
24
+ # 5 mb is a very large photo it will probably never be reached. But
25
+ # this will prevent people from passing off very large files as an image.
26
+ # If you change this limit please document the reason for changing it below
27
+ (1..5).step { |x| bytes_to_megabytes x }
28
+ end
29
+
30
+ version :preview do
31
+ process resize_to_limit: PREVIEW_LIMIT
32
+ end
33
+
34
+ version :post_image do
35
+ process resize_to_limit: POST_LIMIT
36
+ end
37
+
38
+ private
39
+
40
+ def bytes_to_megabytes(bytes)
41
+ bytes * (1024.0 * 1024.0)
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-github-pages-gem
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - MSOE SSE Web Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - Gemfile
20
+ - Gemfile.lock
21
+ - README.md
22
+ - Rakefile
23
+ - jekyll_github_pages.gemspec
24
+ - lib/factories/post_factory.rb
25
+ - lib/jekyll_github_pages.rb
26
+ - lib/models/post.rb
27
+ - lib/models/post_image_manager.rb
28
+ - lib/services/github_service.rb
29
+ - lib/services/kramdown_service.rb
30
+ - lib/services/post_services/base_post_service.rb
31
+ - lib/services/post_services/post_creation_service.rb
32
+ - lib/services/post_services/post_editing_service.rb
33
+ - lib/services/post_services/post_pull_request_editing_service.rb
34
+ - lib/uploaders/post_image_uploader.rb
35
+ homepage:
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 2.7.6.2
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: A gem that uses the github API to make edits with a jekyll blog
59
+ test_files: []