jekyll-uj-powertools 1.6.24 → 1.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e244425c0a57091b17d74eeb82b585314a5892bc4d2185e6dc50b5d66d7d16c
4
- data.tar.gz: e999ee1ea01bee859c675e451c3d8323c984b225a7ae2438f3d06f1f43158848
3
+ metadata.gz: a39cadef84f1c25e0d674e7004e3f6f041f94d27d5df354eea0f31d1c86bc8e3
4
+ data.tar.gz: 12f5642a57b3c8bd4dd7a62f1392c95fa665871c2dc304fdd2f2f75432619d40
5
5
  SHA512:
6
- metadata.gz: 4042544bbfb0c7e818b9d56ab22adb617c0e5ab74f5284e03eaab1c2a5fed114b77e3d7860759cdef9d0ba9d057b542e5e916f690bda237defbf7426056fcff7
7
- data.tar.gz: cd588783c0bd5b02069eb47363352d6200d03de8b4208004a34a23db7372a123dffc47b2138174e7969f4380cf39f649ac977c06a41e6dfbedd7469592732a5f
6
+ metadata.gz: e2c52117d2069807c586ad07e37792fca82216786528d1ecea4d304941f559fa808bc2b8c4bb2eac20dc84c1a9135956fe1c1db35651386336a3ec6053e3ef5c
7
+ data.tar.gz: a62ca9ec4f212468d6c2e8ade617843907dd46c68f76b328d3e0351424c286554a974ec63add70f4f1931e0e38859c7fe83c7f26b4a1004fa56fbeda607601ea
data/README.md CHANGED
@@ -294,6 +294,122 @@ Creates language-specific URLs for multilingual sites.
294
294
  {% uj_translation_url target_lang, "/pricing" %}
295
295
  ```
296
296
 
297
+ ## Generators
298
+
299
+ ### Dynamic Pages Generator
300
+ Automatically generate pages from collection data based on frontmatter fields. This is useful for creating category, tag, or any taxonomy pages dynamically without manually creating each page.
301
+
302
+ #### Configuration
303
+ Add to your `_config.yml`:
304
+
305
+ ```yaml
306
+ generators:
307
+ collection_categories:
308
+ # Example 1: Extract category from nested frontmatter field
309
+ - collection: recipes
310
+ field: recipe.cuisine # Dot notation for nested fields
311
+ layout: recipe-category
312
+ permalink: /recipes/:slug
313
+ title: ":name Recipes"
314
+ description: "Browse our collection of :name recipes."
315
+
316
+ # Example 2: Simple frontmatter field
317
+ - collection: products
318
+ field: category
319
+ layout: product-category
320
+ permalink: /products/:slug
321
+ title: ":name Products"
322
+ description: "Shop our :name products."
323
+
324
+ # Example 3: Minimal config (uses defaults)
325
+ - collection: articles
326
+ field: category
327
+ layout: article-category
328
+ ```
329
+
330
+ #### Template Variables
331
+ Use these placeholders in `title`, `description`, and `permalink`:
332
+ - `:name` - The formatted category name (e.g., "Asian Cuisine")
333
+ - `:slug` - The URL-safe slug (e.g., "asian-cuisine")
334
+
335
+ #### Defaults
336
+ - `permalink`: `/:collection/:slug` (e.g., `/recipes/asian`)
337
+ - `title`: `:name`
338
+ - `description`: `Browse our collection of :name.`
339
+
340
+ #### Generated Page Data
341
+ Each generated page includes the following data accessible in the layout:
342
+ - `page.title` - The formatted title
343
+ - `page.description` - The formatted description
344
+ - `page.category_slug` - URL-safe slug (e.g., "asian-cuisine")
345
+ - `page.category_name` - Human-readable name (e.g., "Asian Cuisine")
346
+ - `page.collection_name` - Source collection name (e.g., "recipes")
347
+ - `page.meta.title` - SEO title with site name appended
348
+ - `page.meta.description` - SEO description
349
+
350
+ #### Example Layout
351
+ Create a layout file (e.g., `_layouts/recipe-category.html`) to display the category page:
352
+
353
+ ```liquid
354
+ ---
355
+ layout: default
356
+ ---
357
+ <h1>{{ page.category_name }}</h1>
358
+ <p>{{ page.description }}</p>
359
+
360
+ {% assign items = site.recipes | where_exp: "item", "item.recipe.cuisine == page.category_name" %}
361
+ {% for item in items %}
362
+ <article>
363
+ <h2><a href="{{ item.url }}">{{ item.title }}</a></h2>
364
+ </article>
365
+ {% endfor %}
366
+ ```
367
+
368
+ ## Parallel Build
369
+ Speed up Jekyll builds by rendering pages and documents in parallel using multiple CPU threads. This can significantly reduce build times for sites with many pages.
370
+
371
+ **Enabled by default** - no configuration needed! The plugin automatically uses all available CPU cores.
372
+
373
+ ### Configuration
374
+ Customize parallel build behavior in `_config.yml`:
375
+
376
+ ```yaml
377
+ parallel_build:
378
+ enabled: true # Enable/disable parallel builds (default: true)
379
+ threads: 8 # Number of threads (default: number of CPU cores)
380
+ min_items: 1 # Minimum items before parallelizing (default: 1)
381
+ ```
382
+
383
+ ### Disabling Parallel Build
384
+ If you encounter issues with thread-safety in custom Liquid tags:
385
+
386
+ ```yaml
387
+ # Option 1: Disable entirely
388
+ parallel_build: false
389
+
390
+ # Option 2: Disable via enabled flag
391
+ parallel_build:
392
+ enabled: false
393
+ ```
394
+
395
+ ### Performance Tips
396
+ - Parallel builds work best with CPU-bound Liquid rendering
397
+ - For maximum speed, combine with `limit_collections` during development
398
+ - Use `--profile` to identify slow templates that benefit most from parallelization
399
+
400
+ ## Development Config (`_config.dev.yml`)
401
+ Speed up dev builds by limiting collections. Create `_config.dev.yml` in your Jekyll source:
402
+
403
+ ```yaml
404
+ limit_collections:
405
+ recipes: 50
406
+ products: 20
407
+ ```
408
+
409
+ Run with: `bundle exec jekyll serve --config _config.yml,_config.dev.yml`
410
+
411
+ UJ auto-loads this file in dev mode.
412
+
297
413
  ## Final notes
298
414
  These examples show how you can use the features of `jekyll-uj-powertools` in your Jekyll site.
299
415
 
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  Gem::Specification.new do |spec|
6
6
  # Gem info
7
7
  spec.name = "jekyll-uj-powertools"
8
- spec.version = "1.6.24"
8
+ spec.version = "1.7.1"
9
9
 
10
10
  # Author info
11
11
  spec.authors = ["ITW Creative Works"]
@@ -31,10 +31,14 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "rake"
32
32
  spec.add_development_dependency "rspec"
33
33
  spec.add_development_dependency "simplecov"
34
+ spec.add_development_dependency "ostruct"
34
35
 
35
36
  # Translation and HTML manipulation requires Nokogiri
36
37
  spec.add_runtime_dependency 'nokogiri', '>= 1.17'
37
38
 
39
+ # Parallel processing for faster builds
40
+ spec.add_runtime_dependency 'parallel', '>= 1.20'
41
+
38
42
  # Ruby version
39
43
  spec.required_ruby_version = ">= 2.0.0"
40
44
  end
@@ -0,0 +1,152 @@
1
+ # Dynamic Pages Generator
2
+ # Automatically generates pages from collection data based on config
3
+ #
4
+ # Usage in _config.yml:
5
+ #
6
+ # generators:
7
+ # collection_categories:
8
+ # # Example 1: Extract category from frontmatter field
9
+ # - collection: recipes
10
+ # field: recipe.cuisine # Dot notation for nested fields
11
+ # layout: recipe-category
12
+ # permalink: /recipes/:slug
13
+ # title: ":name Recipes"
14
+ # description: "Browse our collection of :name recipes."
15
+ #
16
+ # # Example 2: Simple frontmatter field
17
+ # - collection: products
18
+ # field: category
19
+ # layout: product-category
20
+ # permalink: /products/:slug
21
+ # title: ":name Products"
22
+ # description: "Shop our :name products."
23
+ #
24
+ # # Example 3: Minimal config (uses defaults)
25
+ # - collection: articles
26
+ # field: category
27
+ # layout: article-category
28
+ #
29
+ # Template variables for title/description/permalink:
30
+ # :name - The formatted category name (e.g., "Asian Cuisine")
31
+ # :slug - The URL-safe slug (e.g., "asian-cuisine")
32
+ #
33
+ # Defaults:
34
+ # permalink: /:collection/:slug (e.g., /recipes/asian)
35
+ # title: :name
36
+ # description: "Browse our collection of :name."
37
+
38
+ module Jekyll
39
+ class DynamicPagesGenerator < Generator
40
+ safe true
41
+ priority :normal # Run before InjectProperties (:low) so dynamic pages get resolved data
42
+
43
+ def generate(site)
44
+ config = site.config.dig('generators', 'collection_categories')
45
+ return unless config.is_a?(Array)
46
+
47
+ config.each do |category_config|
48
+ generate_category_pages(site, category_config)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def generate_category_pages(site, config)
55
+ collection_name = config['collection']
56
+ field = config['field']
57
+ return unless collection_name && field
58
+
59
+ collection = site.collections[collection_name]
60
+ return unless collection
61
+
62
+ # Configuration with defaults
63
+ layout = config['layout']
64
+ permalink_template = config['permalink'] || "/#{collection_name}/:slug"
65
+ title_template = config['title'] || ':name'
66
+ description_template = config['description'] || 'Browse our collection of :name.'
67
+
68
+ # Extract unique categories from documents
69
+ categories = {}
70
+
71
+ collection.docs.each do |doc|
72
+ category_value = dig_value(doc.data, field)
73
+ next unless category_value && !category_value.empty?
74
+
75
+ category_slug = slugify(category_value)
76
+ next if categories.key?(category_slug)
77
+
78
+ # Format category name: titleize
79
+ category_name = titleize(category_value)
80
+
81
+ # Apply templates
82
+ permalink = apply_template(permalink_template, category_name, category_slug)
83
+ title = apply_template(title_template, category_name, category_slug)
84
+ description = apply_template(description_template, category_name, category_slug)
85
+
86
+ categories[category_slug] = {
87
+ 'slug' => category_slug,
88
+ 'name' => category_name,
89
+ 'title' => title,
90
+ 'description' => description,
91
+ 'permalink' => permalink
92
+ }
93
+ end
94
+
95
+ # Generate a page for each category
96
+ categories.each do |slug, data|
97
+ page = DynamicCategoryPage.new(site, layout, data, collection_name)
98
+ site.pages << page
99
+ end
100
+
101
+ Jekyll.logger.info "DynamicPages:", "Generated #{categories.size} category pages for '#{collection_name}'"
102
+ end
103
+
104
+ # Dig into nested hash using dot notation (e.g., "recipe.cuisine")
105
+ def dig_value(hash, field)
106
+ keys = field.split('.')
107
+ value = hash
108
+ keys.each do |key|
109
+ return nil unless value.is_a?(Hash)
110
+ value = value[key]
111
+ end
112
+ value.is_a?(String) ? value : nil
113
+ end
114
+
115
+ def slugify(str)
116
+ str.to_s.downcase.strip.gsub(/[^\w\s-]/, '').gsub(/[\s_]+/, '-')
117
+ end
118
+
119
+ def titleize(str)
120
+ str.to_s.split(/[\s_-]+/).map(&:capitalize).join(' ')
121
+ end
122
+
123
+ def apply_template(template, name, slug)
124
+ template.gsub(':name', name).gsub(':slug', slug)
125
+ end
126
+ end
127
+
128
+ # Custom page class for dynamically generated category pages
129
+ class DynamicCategoryPage < Page
130
+ def initialize(site, layout, data, collection_name)
131
+ @site = site
132
+ @base = site.source
133
+ @dir = ''
134
+ @name = "#{data['slug']}.html"
135
+
136
+ self.process(@name)
137
+ self.data = {
138
+ 'layout' => layout,
139
+ 'title' => data['title'],
140
+ 'description' => data['description'],
141
+ 'category_slug' => data['slug'],
142
+ 'category_name' => data['name'],
143
+ 'collection_name' => collection_name,
144
+ 'permalink' => data['permalink'],
145
+ 'meta' => {
146
+ 'title' => "#{data['title']} - #{site.config.dig('brand', 'name') || site.config['title']}",
147
+ 'description' => data['description']
148
+ }
149
+ }
150
+ end
151
+ end
152
+ end
@@ -5,7 +5,7 @@
5
5
  module Jekyll
6
6
  class InjectProperties < Generator
7
7
  safe true
8
- priority :low
8
+ priority :lowest # Run LAST so all pages from other generators exist before injecting resolved data
9
9
 
10
10
  def generate(site)
11
11
  # Define a global variable accessible in templates
@@ -0,0 +1,56 @@
1
+ # Limit Collections Generator
2
+ # Limits the number of documents in collections during development for faster builds
3
+ #
4
+ # Usage in _config.dev.yml:
5
+ # limit_collections:
6
+ # recipes: 50
7
+ # products: 20
8
+ #
9
+ # By default, uses a seeded random sample for diverse selection across categories.
10
+ # To disable randomization and take first N documents:
11
+ # limit_collections:
12
+ # recipes: 50
13
+ # products: 20
14
+ # randomize: false
15
+ #
16
+ # Run Jekyll with: bundle exec jekyll serve --config _config.yml,_config.dev.yml
17
+
18
+ module Jekyll
19
+ class LimitCollectionsGenerator < Generator
20
+ safe true
21
+ priority :highest # Run before other generators
22
+
23
+ def generate(site)
24
+ limits = site.config['limit_collections']
25
+ return unless limits.is_a?(Hash)
26
+
27
+ # Check if randomization is disabled (default: true)
28
+ randomize = limits.fetch('randomize', true)
29
+
30
+ limits.each do |collection_name, limit|
31
+ # Skip the 'randomize' option itself
32
+ next if collection_name == 'randomize'
33
+ next unless limit.is_a?(Integer) && limit > 0
34
+
35
+ collection = site.collections[collection_name]
36
+ next unless collection
37
+
38
+ original_count = collection.docs.size
39
+ next if original_count <= limit
40
+
41
+ if randomize
42
+ # Use a seeded random for repeatable but diverse sampling
43
+ # Seed based on collection name so it's consistent across rebuilds
44
+ rng = Random.new(collection_name.hash.abs)
45
+ sampled_docs = collection.docs.shuffle(random: rng).first(limit)
46
+ collection.docs.replace(sampled_docs)
47
+ Jekyll.logger.info "LimitCollections:", "Limited '#{collection_name}' from #{original_count} to #{limit} documents (random sample)"
48
+ else
49
+ # Take first N documents in order
50
+ collection.docs.replace(collection.docs.first(limit))
51
+ Jekyll.logger.info "LimitCollections:", "Limited '#{collection_name}' from #{original_count} to #{limit} documents"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,129 @@
1
+ # Parallel Build Generator
2
+ # Speeds up Jekyll builds by rendering pages and documents in parallel
3
+ #
4
+ # This runs as a Generator with :lowest priority to ensure it processes
5
+ # all pages AFTER other generators (like DynamicPages) have created them.
6
+ #
7
+ # Configuration in _config.yml:
8
+ #
9
+ # parallel_build:
10
+ # enabled: true # Enable/disable parallel builds (default: true)
11
+ # threads: 8 # Number of threads (default: number of CPU cores)
12
+ # min_items: 1 # Minimum items before parallelizing (default: 1)
13
+ #
14
+ # Note: Parallel builds work best with CPU-bound rendering.
15
+ # Some Liquid tags may not be thread-safe - if you encounter issues,
16
+ # disable with `parallel_build: false` or `parallel_build.enabled: false`
17
+
18
+ require 'parallel'
19
+
20
+ module Jekyll
21
+ class ParallelBuildGenerator < Generator
22
+ safe true
23
+ priority :lowest # Run AFTER all other generators
24
+
25
+ def generate(site)
26
+ config = site.config['parallel_build']
27
+
28
+ # Handle both `parallel_build: false` and `parallel_build.enabled: false`
29
+ if config == false
30
+ Jekyll.logger.info "ParallelBuild:", "Disabled via config"
31
+ return
32
+ end
33
+
34
+ config = {} if config.nil? || config == true
35
+ return unless config.fetch('enabled', true)
36
+
37
+ threads = config.fetch('threads', Parallel.processor_count)
38
+ min_items = config.fetch('min_items', 1)
39
+
40
+ # Render documents in parallel (posts, collections)
41
+ render_documents_parallel(site, threads, min_items)
42
+
43
+ # Render pages in parallel
44
+ render_pages_parallel(site, threads, min_items)
45
+ end
46
+
47
+ private
48
+
49
+ def render_documents_parallel(site, threads, min_items)
50
+ # Collect all documents that need rendering
51
+ documents = site.collections.flat_map do |_name, collection|
52
+ collection.docs.select { |doc| doc.respond_to?(:render) && !doc.data['rendered_parallel'] }
53
+ end
54
+
55
+ return if documents.size < min_items
56
+
57
+ Jekyll.logger.info "ParallelBuild:", "Rendering #{documents.size} documents with #{threads} threads..."
58
+
59
+ start_time = Time.now
60
+
61
+ # Pre-render: prepare payload and info for each document
62
+ # We need to do the actual Liquid rendering in parallel
63
+ Parallel.each(documents, in_threads: threads) do |doc|
64
+ begin
65
+ # Mark as rendered to avoid double-rendering
66
+ doc.data['rendered_parallel'] = true
67
+
68
+ # Render content through Liquid
69
+ if doc.content && !doc.content.empty?
70
+ payload = site.site_payload
71
+ info = {
72
+ filters: [Jekyll::Filters],
73
+ registers: {
74
+ site: site,
75
+ page: doc.to_liquid
76
+ }
77
+ }
78
+
79
+ # Parse and render Liquid template
80
+ template = site.liquid_renderer.file(doc.path).parse(doc.content)
81
+ doc.content = template.render!(payload, info)
82
+ end
83
+ rescue => e
84
+ Jekyll.logger.warn "ParallelBuild:", "Error rendering #{doc.relative_path}: #{e.message}"
85
+ end
86
+ end
87
+
88
+ elapsed = Time.now - start_time
89
+ Jekyll.logger.info "ParallelBuild:", "Documents rendered in #{elapsed.round(2)}s"
90
+ end
91
+
92
+ def render_pages_parallel(site, threads, min_items)
93
+ # Get all pages (including dynamically generated ones without content)
94
+ pages = site.pages.reject { |page| page.data['rendered_parallel'] }
95
+
96
+ return if pages.size < min_items
97
+
98
+ Jekyll.logger.info "ParallelBuild:", "Rendering #{pages.size} pages with #{threads} threads..."
99
+
100
+ start_time = Time.now
101
+
102
+ Parallel.each(pages, in_threads: threads) do |page|
103
+ begin
104
+ page.data['rendered_parallel'] = true
105
+
106
+ # Only render if there's content to process
107
+ if page.content && !page.content.empty?
108
+ payload = site.site_payload
109
+ info = {
110
+ filters: [Jekyll::Filters],
111
+ registers: {
112
+ site: site,
113
+ page: page.to_liquid
114
+ }
115
+ }
116
+
117
+ template = site.liquid_renderer.file(page.path).parse(page.content)
118
+ page.content = template.render!(payload, info)
119
+ end
120
+ rescue => e
121
+ Jekyll.logger.warn "ParallelBuild:", "Error rendering #{page.name}: #{e.message}"
122
+ end
123
+ end
124
+
125
+ elapsed = Time.now - start_time
126
+ Jekyll.logger.info "ParallelBuild:", "Pages rendered in #{elapsed.round(2)}s"
127
+ end
128
+ end
129
+ end
@@ -1,17 +1,21 @@
1
1
  # Libraries
2
2
  require "jekyll"
3
+ require "parallel"
3
4
 
4
5
  module Jekyll
5
6
  # Load Filters
6
7
  require_relative "filters/main"
7
8
 
8
9
  # Load Generators
10
+ require_relative "generators/limit-collections"
9
11
  require_relative "generators/inject-properties"
10
12
  require_relative "generators/blog-taxonomy"
13
+ require_relative "generators/dynamic-pages"
11
14
 
12
15
  # Load Hooks
13
16
  require_relative "hooks/inject-properties"
14
17
  require_relative "hooks/markdown-images"
18
+ require_relative "hooks/parallel-build"
15
19
 
16
20
  # Load Tags
17
21
  require_relative "tags/external"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-uj-powertools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.24
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITW Creative Works
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-12-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: jekyll
@@ -86,6 +85,20 @@ dependencies:
86
85
  - - ">="
87
86
  - !ruby/object:Gem::Version
88
87
  version: '0'
88
+ - !ruby/object:Gem::Dependency
89
+ name: ostruct
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
89
102
  - !ruby/object:Gem::Dependency
90
103
  name: nokogiri
91
104
  requirement: !ruby/object:Gem::Requirement
@@ -100,6 +113,20 @@ dependencies:
100
113
  - - ">="
101
114
  - !ruby/object:Gem::Version
102
115
  version: '1.17'
116
+ - !ruby/object:Gem::Dependency
117
+ name: parallel
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '1.20'
123
+ type: :runtime
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '1.20'
103
130
  description: jekyll-uj-powertools provides a powerful set of utilities for Jekyll,
104
131
  including functions to remove ads from strings and escape JSON characters.
105
132
  email:
@@ -115,10 +142,13 @@ files:
115
142
  - jekyll-uj-powertools.gemspec
116
143
  - lib/filters/main.rb
117
144
  - lib/generators/blog-taxonomy.rb
145
+ - lib/generators/dynamic-pages.rb
118
146
  - lib/generators/inject-properties.rb
147
+ - lib/generators/limit-collections.rb
119
148
  - lib/helpers/variable_resolver.rb
120
149
  - lib/hooks/inject-properties.rb
121
150
  - lib/hooks/markdown-images.rb
151
+ - lib/hooks/parallel-build.rb
122
152
  - lib/jekyll-uj-powertools.rb
123
153
  - lib/tags/external.rb
124
154
  - lib/tags/fake_comments.rb
@@ -140,7 +170,6 @@ homepage: https://github.com/itw-creative-works/jekyll-uj-powertools
140
170
  licenses:
141
171
  - MIT
142
172
  metadata: {}
143
- post_install_message:
144
173
  rdoc_options: []
145
174
  require_paths:
146
175
  - lib
@@ -155,8 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
184
  - !ruby/object:Gem::Version
156
185
  version: '0'
157
186
  requirements: []
158
- rubygems_version: 3.2.3
159
- signing_key:
187
+ rubygems_version: 4.0.3
160
188
  specification_version: 4
161
189
  summary: A powerful set of utilities for Jekyll
162
190
  test_files: []