cloudcannon-jekyll 2.3.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,281 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "jekyll"
4
- require "fileutils"
5
- require_relative "reader"
3
+ require 'jekyll'
4
+ require 'json'
5
+ require 'fileutils'
6
+ require_relative 'logger'
7
+ require_relative 'config'
8
+ require_relative 'generators/info'
6
9
 
7
10
  module CloudCannonJekyll
8
- # Generates JSON files containing build config and build output details
11
+ # Generates JSON file with build information
9
12
  class Generator < Jekyll::Generator
10
- # Override the Jekyll::Plugin spaceship to push our plugin to the very end
11
13
  priority :lowest
14
+
15
+ # Override the Jekyll::Plugin spaceship to run at the end
12
16
  def self.<=>(*)
13
17
  1
14
18
  end
15
19
 
16
- # rubocop:disable Metrics/MethodLength
17
20
  def generate(site)
18
- log "⭐️ Starting #{"cloudcannon-jekyll".blue}"
19
-
21
+ Logger.info "⭐️ Starting #{'cloudcannon-jekyll'.blue}"
20
22
  @site = site
21
- @reader = Reader.new(@site)
22
-
23
- migrate_legacy_config
24
- pages = generate_pages
25
- collections_config = generate_collections_config(pages)
26
- drafts = add_blogging_config(collections_config)
27
- add_collection_paths(collections_config)
28
-
29
- collections = generate_collections(collections_config, pages, drafts)
30
- remove_empty_collection_config(collections_config, collections)
31
-
32
- add_data_config(collections_config)
33
- data = generate_data
34
-
35
- generate_file("info", @site.site_payload.merge({
36
- "pwd" => Dir.pwd,
37
- "version" => "0.0.2",
38
- "gem_version" => CloudCannonJekyll::VERSION,
39
- "config" => @site.config,
40
- "collections_config" => collections_config,
41
- "collections" => collections,
42
- "data" => data,
43
- }))
44
- end
45
- # rubocop:enable Metrics/MethodLength
46
-
47
- # rubocop:disable Metrics/CyclomaticComplexity
48
- def generate_collections_config(pages)
49
- collections = @site.config["collections"]&.dup || {}
50
- collections_config = @site.config.dig("cloudcannon", "collections")&.dup || {}
51
-
52
- collections.each_key do |key|
53
- # Workaround for empty collection configurations
54
- defaults = collections[key] || { "output" => false }
55
- collections_config[key] = (collections_config[key] || {}).merge(defaults)
56
- end
57
-
58
- unless pages.empty? || collections.key?("pages")
59
- pages_defaults = {
60
- "output" => true,
61
- "filter" => "strict",
62
- "path" => "",
63
- }
64
-
65
- collections_config["pages"] = pages_defaults.merge(collections_config["pages"] || {})
66
- end
67
-
68
- collections_config
69
- end
70
-
71
- # rubocop:disable Metrics/PerceivedComplexity
72
- def generate_collections(collections_config, pages, drafts)
73
- split_posts = group_by_category_folder(all_posts, "posts")
74
- split_drafts = group_by_category_folder(drafts, "drafts")
75
-
76
- collections = {}
77
- collections_config.each_key do |key|
78
- collections[key] = if key == "posts" || key.end_with?("/posts")
79
- split_posts[key]
80
- elsif key == "drafts" || key.end_with?("/drafts")
81
- split_drafts[key]
82
- else
83
- @site.collections[key]&.docs
84
- end
85
-
86
- collections[key] ||= []
87
- end
88
-
89
- has_collection_pages = collections.key?("pages") && !collections["pages"].empty?
90
- collections["pages"] = pages unless pages.empty? || has_collection_pages
91
- collections
92
- end
93
- # rubocop:enable Metrics/PerceivedComplexity
94
- # rubocop:enable Metrics/CyclomaticComplexity
95
-
96
- def generate_data
97
- cc_data = @site.config.dig("cloudcannon", "data")
98
- data = if cc_data == true
99
- @site.data&.dup
100
- elsif cc_data&.is_a?(Hash)
101
- @site.data&.select { |key, _| cc_data.key?(key) }
102
- end
103
-
104
- data ||= {}
105
- data["categories"] ||= @site.categories.keys
106
- data["tags"] ||= @site.tags.keys
107
-
108
- data.each_key do |key|
109
- log "💾 Processed #{key.bold} data set"
110
- end
111
-
112
- data
113
- end
114
-
115
- def generate_pages
116
- html_pages = @site.pages.select do |page|
117
- page.html? || page.url.end_with?("/")
118
- end
119
-
120
- static_pages = @site.static_files.select do |static_page|
121
- JsonifyFilter::STATIC_EXTENSIONS.include?(static_page.extname)
122
- end
123
-
124
- html_pages + static_pages
125
- end
126
-
127
- def collections_dir
128
- return "" if Jekyll::VERSION.start_with? "2."
129
-
130
- @site.config["collections_dir"] || ""
131
- end
132
-
133
- def data_dir
134
- @site.config["data_dir"] || "_data"
135
- end
136
-
137
- def all_posts
138
- posts = @site.posts || @site.collections["posts"]
139
- posts.class.method_defined?(:docs) ? posts.docs : posts
140
- end
141
-
142
- def group_by_category_folder(collection, key)
143
- split_path = "/_#{key}/"
144
- collection.group_by do |doc|
145
- parts = doc.relative_path.split(split_path)
146
- if parts.length > 1
147
- "#{parts.first}/#{key}".sub(%r!^\/+!, "")
148
- else
149
- key
150
- end
151
- end
152
- end
153
-
154
- def add_category_folder_config(collections_config, posts_config = {})
155
- seen = {}
156
-
157
- all_posts.map do |post|
158
- parts = post.relative_path.split("/_posts/")
159
- path = parts.first
160
-
161
- # Ignore unless it's an unseen category folder post
162
- next if parts.length < 2 || path.empty? || seen[path]
163
-
164
- # Could check this to ensure raw files exist since posts can be generated without files
165
- # next if @reader.read_posts(parts[0]).empty?
166
-
167
- seen[path] = true
168
- folder = path.sub(%r!^\/+!, "")
169
- collections_path = "#{collections_dir}/#{folder}".gsub(%r!\/+!, "/").sub(%r!^\/+!, "")
170
-
171
- collections_config["#{folder}/posts"] = posts_config.merge({
172
- "path" => "#{collections_path}/_posts",
173
- })
174
-
175
- # Adding the category draft config like this isn't ideal, since you could have drafts
176
- # without posts, but it's a decent trade off vs looking for _drafts folders
177
- collections_config["#{folder}/drafts"] = posts_config.merge({
178
- "path" => "#{collections_path}/_drafts",
179
- })
180
-
181
- path
182
- end
183
- end
184
-
185
- def remove_empty_collection_config(collections_config, collections)
186
- cc_collections = @site.config.dig("cloudcannon", "collections") || {}
187
-
188
- collections_config.each_key do |key|
189
- if collections[key].empty? && !cc_collections.key?(key)
190
- log "📂 #{"Ignored".yellow} #{key.bold} collection"
191
- collections_config.delete(key)
192
- else
193
- log "📁 Processed #{key.bold} collection with #{collections[key]&.length || 0} files"
194
- end
195
- end
196
- end
197
-
198
- def migrate_legacy_config
199
- add_legacy_explore_groups
200
- end
201
-
202
- # Support for the deprecated _explore configuration
203
- def add_legacy_explore_groups
204
- unless @site.config.key?("_collection_groups")
205
- @site.config["_collection_groups"] = @site.config.dig("_explore", "groups")&.dup
206
- end
207
- end
208
-
209
- # Add data to collections config if raw data files exist
210
- def add_data_config(collections_config)
211
- data_files = @reader.read_data(data_dir)
212
- collections_config["data"] = { "path" => data_dir } if data_files&.keys&.any?
213
- end
214
-
215
- # Add posts/drafts to collections config
216
- def add_blogging_config(collections_config)
217
- collections_config["posts"] = { "output" => true } if Jekyll::VERSION.start_with? "2."
218
- drafts = @reader.read_drafts(collections_dir)
219
-
220
- if drafts.any? && !collections_config.key?("drafts")
221
- collections_config["drafts"] = { "output" => !!@site.show_drafts }
222
- end
223
-
224
- folders = add_category_folder_config(collections_config, collections_config["posts"])
225
- folders.compact.each do |folder|
226
- drafts += @reader.read_drafts(folder)
227
- end
228
-
229
- drafts
230
- end
231
-
232
- # Add path to each collection config
233
- def add_collection_paths(collections_config)
234
- collections_config.each do |key, collection|
235
- collection["path"] ||= File.join(collections_dir, "_#{key}").sub(%r!^\/+!, "")
236
- end
23
+ config = Config.new(site)
24
+ generate_file('info', Info.new.generate_info(site, config.read))
237
25
  end
238
26
 
239
27
  def generate_file(filename, data)
240
28
  dest = destination_path(filename)
241
29
  FileUtils.mkdir_p(File.dirname(dest))
242
- File.open(dest, "w") { |file| file.write(file_content(filename, data)) }
30
+ File.open(dest, 'w') { |file| file.write(file_content(data)) }
243
31
  @site.keep_files ||= []
244
32
  @site.keep_files << path(filename)
245
- log "🏁 Generated #{path(filename).bold} #{"successfully".green}"
246
- end
247
-
248
- def log(str)
249
- Jekyll.logger.info("CloudCannon:", str)
250
- end
251
-
252
- def version_path_suffix
253
- return "-2.x" if Jekyll::VERSION.start_with? "2."
254
- return "-3.0-4.x" if Jekyll::VERSION.match? %r!3\.[0-4]\.!
255
-
256
- ""
33
+ Logger.info "🏁 Generated #{path(filename).bold} #{'successfully'.green}"
257
34
  end
258
35
 
259
- def path(filename, suffix = "")
260
- "_cloudcannon/#{filename}#{suffix}.json"
36
+ def path(filename)
37
+ "_cloudcannon/#{filename}.json"
261
38
  end
262
39
 
263
40
  def source_path(filename)
264
- File.expand_path(path(filename, version_path_suffix), File.dirname(__FILE__))
41
+ file_path = path(filename)
42
+ File.expand_path(file_path, File.dirname(__FILE__))
265
43
  end
266
44
 
267
45
  def destination_path(filename)
268
46
  Jekyll.sanitized_path(@site.dest, path(filename))
269
47
  end
270
48
 
271
- def file_content(filename, data)
272
- page = PageWithoutAFile.new(@site, File.dirname(__FILE__), "", path(filename))
273
- page.content = File.read(source_path(filename))
274
- page.data["layout"] = nil
275
- page.data["sitemap"] = false
276
- page.data["permalink"] = "/#{path(filename)}"
277
- page.render({}, data)
278
- page.output
49
+ def file_content(data)
50
+ JSON.pretty_generate(data)
279
51
  end
280
52
  end
281
53
  end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative '../readers/reader'
5
+ require_relative '../logger'
6
+ require_relative 'paths'
7
+
8
+ module CloudCannonJekyll
9
+ STATIC_EXTENSIONS = ['.html', '.htm'].freeze
10
+ IS_JEKYLL_2_X_X = Jekyll::VERSION.start_with?('2.').freeze
11
+ IS_JEKYLL_3_04_X = Jekyll::VERSION.match?(/3\.[0-4]\./).freeze
12
+
13
+ # Helper functions for generating collection configuration and summaries
14
+ class Collections
15
+ def initialize(site, config)
16
+ @site = site
17
+ @config = config
18
+ @reader = Reader.new(site)
19
+ @collections_dir = Paths.collections_dir(site)
20
+ @data_dir = Paths.data_dir(site)
21
+ @split_posts = group_by_category_folder(all_posts, 'posts')
22
+ @split_drafts = group_by_category_folder(all_drafts, 'drafts')
23
+ end
24
+
25
+ def generate_collections_config
26
+ collections = @site.config['collections'] || {}
27
+ collections_config = @config['collections_config']&.dup || {}
28
+
29
+ return collections_config if @config['collections_config_override']
30
+
31
+ if collections.is_a?(Array)
32
+ collections = collections.each_with_object({}) { |key, memo| memo[key] = {} }
33
+ end
34
+
35
+ defaults = {
36
+ 'data' => {
37
+ 'path' => @data_dir,
38
+ 'output' => false
39
+ },
40
+ 'posts' => {
41
+ 'output' => true
42
+ },
43
+ 'drafts' => {
44
+ 'output' => !!@site.show_drafts
45
+ }
46
+ }
47
+
48
+ unless collections.key?('pages') && !collections['pages'].empty?
49
+ defaults['pages'] = {
50
+ 'path' => '',
51
+ 'output' => true,
52
+ 'filter' => 'strict'
53
+ }
54
+ end
55
+
56
+ collection_keys = (defaults.keys + collections.keys).uniq
57
+
58
+ collection_keys.each do |key|
59
+ processed = (defaults[key] || {})
60
+ .merge(collections[key] || {})
61
+ .merge(collections_config[key] || {})
62
+
63
+ processed['output'] ||= false
64
+ processed['auto_discovered'] = !collections_config.key?(key)
65
+ processed['path'] ||= File.join(@collections_dir, "_#{key}")
66
+ processed['path'] = processed['path'].sub(%r{^/+}, '')
67
+
68
+ Config.rename_legacy_collection_config_keys(processed)
69
+
70
+ collections_config[key] = processed
71
+ end
72
+
73
+ @split_posts.each_key do |key|
74
+ posts_path = @split_posts[key]&.first&.relative_path&.sub(%r{(^|/)_posts.*}, '\1_posts')
75
+ next unless posts_path
76
+
77
+ defaults = {
78
+ 'auto_discovered' => !collections_config.key?(key),
79
+ 'path' => File.join(@collections_dir, posts_path).sub(%r{^/+}, ''),
80
+ 'output' => true
81
+ }
82
+
83
+ collections_config[key] = defaults.merge(collections_config[key] || {})
84
+ end
85
+
86
+ @split_drafts.each_key do |key|
87
+ drafts_path = @split_drafts[key]&.first&.relative_path&.sub(%r{(^|/)_drafts.*}, '\1_drafts')
88
+ next unless drafts_path
89
+
90
+ defaults = {
91
+ 'auto_discovered' => !collections_config.key?(key),
92
+ 'path' => File.join(@collections_dir, drafts_path).sub(%r{^/+}, ''),
93
+ 'output' => !!@site.show_drafts
94
+ }
95
+
96
+ collections_config[key] = defaults.merge(collections_config[key] || {})
97
+ end
98
+
99
+ collections_config
100
+ end
101
+
102
+ def drafts_paths
103
+ paths = @split_posts.keys.map do |key|
104
+ File.join('/', @collections_dir, key.sub(/posts$/, '_drafts'))
105
+ end
106
+
107
+ paths.empty? ? [File.join('/', @collections_dir, '_drafts')] : paths
108
+ end
109
+
110
+ def generate_collections(collections_config)
111
+ collections = {}
112
+
113
+ collections_config.each_key do |key|
114
+ next if key == 'data'
115
+
116
+ collections[key] = if key == 'posts' || key.end_with?('/posts')
117
+ @split_posts[key]
118
+ elsif key == 'drafts' || key.end_with?('/drafts')
119
+ @split_drafts[key]
120
+ else
121
+ @site.collections[key]&.docs
122
+ end
123
+
124
+ collections[key] ||= []
125
+ collections[key] = collections[key].map do |doc|
126
+ document_to_json(doc, key)
127
+ end
128
+ end
129
+
130
+ if collections.key?('pages') && collections['pages'].empty?
131
+ collections['pages'] = all_pages.map do |doc|
132
+ document_to_json(doc, 'pages')
133
+ end
134
+ end
135
+
136
+ collections
137
+ end
138
+
139
+ def remove_empty_collection_config(collections_config, collections)
140
+ collections_config.each do |key, collection_config|
141
+ should_delete = if key == 'data'
142
+ !data_files?
143
+ else
144
+ collections[key].empty? && collection_config['auto_discovered']
145
+ end
146
+
147
+ if should_delete
148
+ Logger.info "📂 #{'Ignored'.yellow} #{key.bold} collection"
149
+ collections_config.delete(key)
150
+ else
151
+ count = collections[key]&.length || 0
152
+ Logger.info "📁 Processed #{key.bold} collection with #{count} files"
153
+ end
154
+ end
155
+ end
156
+
157
+ def document_type(doc)
158
+ if IS_JEKYLL_2_X_X && (doc.instance_of?(Jekyll::Post) || doc.instance_of?(Jekyll::Draft))
159
+ :posts
160
+ elsif doc.respond_to? :type
161
+ doc.type
162
+ elsif doc.respond_to?(:collection)
163
+ doc.collection.label.to_sym
164
+ elsif doc.instance_of?(Jekyll::Page)
165
+ :pages
166
+ end
167
+ end
168
+
169
+ def legacy_document_data(doc)
170
+ data = doc.data.merge(
171
+ {
172
+ categories: doc.categories,
173
+ tags: doc.tags,
174
+ date: doc.date
175
+ }
176
+ )
177
+
178
+ data['slug'] = doc.slug if doc.respond_to?(:slug)
179
+ data
180
+ end
181
+
182
+ def legacy_doc?(doc)
183
+ (IS_JEKYLL_3_04_X && doc.instance_of?(Jekyll::Document) && doc.collection.label == 'posts') ||
184
+ (IS_JEKYLL_2_X_X && (doc.instance_of?(Jekyll::Draft) || doc.instance_of?(Jekyll::Post)))
185
+ end
186
+
187
+ def document_data(doc)
188
+ data = if legacy_doc?(doc)
189
+ legacy_document_data(doc)
190
+ elsif doc.respond_to?(:data)
191
+ doc.data
192
+ else
193
+ {}
194
+ end
195
+
196
+ defaults = @site.frontmatter_defaults.all(doc.relative_path, document_type(doc))
197
+ defaults.merge(data)
198
+ end
199
+
200
+ def document_url(doc)
201
+ doc.respond_to?(:url) ? doc.url : doc.relative_path
202
+ end
203
+
204
+ def document_path(doc)
205
+ path = if doc.respond_to?(:collection) && doc.collection
206
+ File.join(@collections_dir, doc.relative_path)
207
+ else
208
+ doc.relative_path
209
+ end
210
+
211
+ path.sub(%r{^/+}, '')
212
+ end
213
+
214
+ def document_to_json(doc, collection)
215
+ base = document_data(doc).merge(
216
+ {
217
+ 'path' => document_path(doc),
218
+ 'url' => document_url(doc),
219
+ 'collection' => collection
220
+ }
221
+ )
222
+
223
+ base['id'] = doc.id if doc.respond_to? :id
224
+ base
225
+ end
226
+
227
+ def all_posts
228
+ posts = @site.posts || @site.collections['posts']
229
+ posts.class.method_defined?(:docs) ? posts.docs : posts
230
+ end
231
+
232
+ def all_drafts
233
+ drafts_paths.reduce([]) do |drafts, drafts_path|
234
+ base_path = drafts_path.gsub(%r{(^|/)_drafts}, '')
235
+ drafts + @reader.read_drafts(base_path)
236
+ end
237
+ end
238
+
239
+ def all_pages
240
+ html_pages = @site.pages.select do |page|
241
+ page.html? || page.url.end_with?('/')
242
+ end
243
+
244
+ static_pages = @site.static_files.select do |static_page|
245
+ STATIC_EXTENSIONS.include?(static_page.extname)
246
+ end
247
+
248
+ html_pages + static_pages
249
+ end
250
+
251
+ def data_files?
252
+ @reader.read_data(@data_dir)&.keys&.any?
253
+ end
254
+
255
+ def group_by_category_folder(collection, key)
256
+ split_path = "/_#{key}/"
257
+ collection.group_by do |doc|
258
+ parts = doc.relative_path.split(split_path)
259
+ if parts.length > 1
260
+ File.join(parts.first, key).sub(%r{^/+}, '')
261
+ else
262
+ key
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../logger'
4
+ require_relative 'paths'
5
+
6
+ module CloudCannonJekyll
7
+ # Generator functions for site data
8
+ class Data
9
+ def initialize(site, config)
10
+ @site = site
11
+ @config = config
12
+ end
13
+
14
+ def generate_data
15
+ data_config = @config['data_config']
16
+ data = case data_config
17
+ when true
18
+ @site.data&.dup
19
+ when Hash
20
+ @site.data&.select { |key, _| data_config.key?(key) && data_config[key] }
21
+ end
22
+
23
+ data ||= {}
24
+ data['categories'] ||= @site.categories.keys
25
+ data['tags'] ||= @site.tags.keys
26
+
27
+ data.each_key do |key|
28
+ Logger.info "💾 Processed #{key.bold} data set"
29
+ end
30
+
31
+ data
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jekyll'
4
+ require 'fileutils'
5
+ require_relative '../logger'
6
+ require_relative 'collections'
7
+ require_relative 'data'
8
+ require_relative 'paths'
9
+
10
+ module CloudCannonJekyll
11
+ # Generates a summary of a Jekyll site
12
+ class Info
13
+ def generate_info(site, config)
14
+ @site = site
15
+ @site_config = site.config
16
+ @config = config
17
+ @data_dir = Paths.data_dir(site)
18
+ @collections_dir = Paths.collections_dir(site)
19
+
20
+ collections_generator = Collections.new(site, config)
21
+ collections_config = collections_generator.generate_collections_config
22
+ collections = collections_generator.generate_collections(collections_config)
23
+ collections_generator.remove_empty_collection_config(collections_config, collections)
24
+
25
+ {
26
+ time: site.time.iso8601,
27
+ version: '0.0.3',
28
+ cloudcannon: generate_cloudcannon,
29
+ generator: generate_generator,
30
+ paths: generate_paths,
31
+ collections_config: collections_config,
32
+ collection_groups: @config['collection_groups'],
33
+ collections: collections,
34
+ data: generate_data,
35
+ source: @config['source'] || '',
36
+ timezone: @config['timezone'],
37
+ base_url: @config['base_url'] || '',
38
+ editor: @config['editor'],
39
+ source_editor: @config['source_editor'],
40
+ _inputs: @config['_inputs'],
41
+ _editables: @config['_editables'],
42
+ _select_data: @config['_select_data'],
43
+ _structures: @config['_structures'],
44
+
45
+ # Deprecated
46
+ _array_structures: @config['_array_structures'],
47
+ _comments: @config['_comments'],
48
+ _enabled_editors: @config['_enabled_editors'],
49
+ _instance_values: @config['_instance_values'],
50
+ _options: @config['_options'],
51
+
52
+ # Jekyll-only
53
+ defaults: @site_config['defaults']
54
+ }.compact
55
+ end
56
+
57
+ def generate_data
58
+ data_generator = Data.new(@site, @config)
59
+ data_generator.generate_data
60
+ end
61
+
62
+ def generate_cloudcannon
63
+ {
64
+ name: 'cloudcannon-jekyll',
65
+ version: CloudCannonJekyll::VERSION
66
+ }
67
+ end
68
+
69
+ def generate_paths
70
+ {
71
+ static: '',
72
+ uploads: @config.dig('paths', 'uploads') || 'uploads',
73
+ data: @data_dir,
74
+ collections: @collections_dir,
75
+ layouts: @site_config['layouts_dir'] || '_layouts'
76
+ }
77
+ end
78
+
79
+ def generate_generator
80
+ {
81
+ name: 'jekyll',
82
+ version: Jekyll::VERSION,
83
+ environment: Jekyll.env,
84
+ metadata: {
85
+ markdown: @site_config['markdown'],
86
+ kramdown: @site_config['kramdown'],
87
+ commonmark: @site_config['commonmark']
88
+ }
89
+ }
90
+ end
91
+ end
92
+ end