joys 0.1.1 → 0.1.3
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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +335 -160
- data/joys-0.1.0.gem +0 -0
- data/joys-0.1.1.gem +0 -0
- data/joys-0.1.2.gem +0 -0
- data/lib/.DS_Store +0 -0
- data/lib/joys/cli.rb +1554 -0
- data/lib/joys/config.rb +133 -0
- data/lib/joys/core.rb +189 -0
- data/lib/joys/data.rb +477 -0
- data/lib/joys/helpers.rb +138 -0
- data/lib/joys/ssg.rb +597 -0
- data/lib/joys/styles.rb +156 -0
- data/lib/joys/tags.rb +124 -0
- data/lib/joys/toys.rb +328 -0
- data/lib/joys/version.rb +1 -1
- data/lib/joys.rb +5 -5
- metadata +13 -2
- data/.DS_Store +0 -0
data/lib/joys/ssg.rb
ADDED
@@ -0,0 +1,597 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# lib/joys/ssg.rb - Static Site Generation for hash-based data
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'json'
|
6
|
+
require 'set'
|
7
|
+
require 'digest'
|
8
|
+
|
9
|
+
module Joys
|
10
|
+
module SSG
|
11
|
+
module_function
|
12
|
+
|
13
|
+
@output_dir = 'dist'
|
14
|
+
@dependencies = { templates: {}, data: {} }
|
15
|
+
@asset_manifest = {}
|
16
|
+
@error_pages = {}
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :output_dir
|
20
|
+
attr_reader :dependencies, :asset_manifest, :error_pages
|
21
|
+
end
|
22
|
+
|
23
|
+
def build(content_dir, output_dir = @output_dir, force: false)
|
24
|
+
#puts "Building static site: #{content_dir} → #{output_dir}"
|
25
|
+
@dependencies = { templates: {}, data: {} }
|
26
|
+
@asset_manifest = {}
|
27
|
+
setup_output_dir(output_dir, force)
|
28
|
+
load_data_definitions(content_dir)
|
29
|
+
load_components_and_layouts(content_dir)
|
30
|
+
pages = discover_pages(content_dir)
|
31
|
+
built_files = build_pages(pages, output_dir, content_dir)
|
32
|
+
build_error_pages(output_dir, content_dir)
|
33
|
+
process_assets(content_dir, output_dir)
|
34
|
+
save_dependencies(output_dir)
|
35
|
+
save_asset_manifest(output_dir)
|
36
|
+
#puts "Built #{built_files.size} pages"
|
37
|
+
built_files
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_incremental(content_dir, changed_files, output_dir = @output_dir)
|
41
|
+
#puts "Incremental build for: #{changed_files.map { |f| File.basename(f) }.join(', ')}"
|
42
|
+
load_dependencies(output_dir)
|
43
|
+
load_asset_manifest(output_dir)
|
44
|
+
affected_pages = find_affected_pages(changed_files)
|
45
|
+
if affected_pages.empty?
|
46
|
+
#puts "No pages affected by changes"
|
47
|
+
return []
|
48
|
+
end
|
49
|
+
if changed_files.any? { |f| f.include?('/data/') }
|
50
|
+
load_data_definitions(content_dir)
|
51
|
+
end
|
52
|
+
if changed_files.any? { |f| f.include?('/_components/') || f.include?('/_layouts/') }
|
53
|
+
load_components_and_layouts(content_dir)
|
54
|
+
end
|
55
|
+
build_pages(affected_pages, output_dir, content_dir)
|
56
|
+
end
|
57
|
+
|
58
|
+
def setup_output_dir(output_dir, force)
|
59
|
+
FileUtils.rm_rf(output_dir) if force && Dir.exist?(output_dir)
|
60
|
+
FileUtils.mkdir_p(output_dir)
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_data_definitions(content_dir)
|
64
|
+
data_dir = File.join(content_dir, '../data')
|
65
|
+
return unless Dir.exist?(data_dir)
|
66
|
+
Joys::Data.configure(data_path: data_dir) if defined?(Joys::Data)
|
67
|
+
Dir.glob("#{data_dir}/**/*_data.rb").each do |file|
|
68
|
+
#puts " Loading data: #{File.basename(file)}"
|
69
|
+
load file
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def load_components_and_layouts(content_dir)
|
74
|
+
Dir.glob("#{content_dir}/**/_components/*.rb").each do |file|
|
75
|
+
#puts " Loading component: #{File.basename(file, '.rb')}"
|
76
|
+
load file
|
77
|
+
end
|
78
|
+
Dir.glob("#{content_dir}/**/_layouts/*.rb").each do |file|
|
79
|
+
#puts " Loading layout: #{File.basename(file, '.rb')}"
|
80
|
+
load file
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def discover_pages(content_dir)
|
85
|
+
pages = []
|
86
|
+
error_pages = []
|
87
|
+
|
88
|
+
Dir.glob("#{content_dir}/**/*.rb").each do |file|
|
89
|
+
next if file.include?('/_components/') ||
|
90
|
+
file.include?('/_layouts/')
|
91
|
+
|
92
|
+
page_info = analyze_page_file(file, content_dir)
|
93
|
+
next unless page_info
|
94
|
+
|
95
|
+
# Check if this is an error page (404.rb, 500.rb, etc.)
|
96
|
+
basename = File.basename(file, '.rb')
|
97
|
+
if basename.match?(/^\d{3}$/) # HTTP status codes
|
98
|
+
page_info[:error_code] = basename.to_i
|
99
|
+
error_pages << page_info
|
100
|
+
else
|
101
|
+
pages << page_info
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Store error pages for later use
|
106
|
+
@error_pages = error_pages.group_by { |p| [p[:domain], p[:error_code]] }
|
107
|
+
|
108
|
+
pages.compact
|
109
|
+
end
|
110
|
+
|
111
|
+
def analyze_page_file(file_path, content_dir)
|
112
|
+
domain_match = file_path.match(%r{#{Regexp.escape(content_dir)}/([^/]+)/})
|
113
|
+
return nil unless domain_match
|
114
|
+
domain = domain_match[1]
|
115
|
+
relative_path = file_path.sub("#{content_dir}/#{domain}/", '')
|
116
|
+
url_path = file_path_to_url(relative_path)
|
117
|
+
{
|
118
|
+
file_path: file_path,
|
119
|
+
domain: domain,
|
120
|
+
url_path: url_path,
|
121
|
+
output_path: url_to_output_path(domain, url_path)
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def file_path_to_url(file_path)
|
126
|
+
path = file_path.sub(/\.rb$/, '')
|
127
|
+
path = path == 'index' ? '/' : "/#{path}/"
|
128
|
+
path.gsub(/_([^\/]+)/, ':\1') # Convert _id to :id (though not used in SSG)
|
129
|
+
end
|
130
|
+
|
131
|
+
def url_to_output_path(domain, url_path)
|
132
|
+
base = domain == 'default' ? '' : "#{domain}/"
|
133
|
+
if url_path == '/'
|
134
|
+
"#{base}index.html"
|
135
|
+
else
|
136
|
+
clean_path = url_path.gsub(/[^a-zA-Z0-9\/\-_]/, '') # Remove special chars
|
137
|
+
"#{base}#{clean_path.sub(/^\//, '').sub(/\/$/, '')}/index.html"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def build_pages(pages, output_dir, content_dir)
|
142
|
+
built_files = []
|
143
|
+
pages.each do |page|
|
144
|
+
page_results = build_single_page(page, content_dir)
|
145
|
+
next if page_results.empty?
|
146
|
+
|
147
|
+
page_results.each do |result|
|
148
|
+
output_file = File.join(output_dir, result[:output_path])
|
149
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
150
|
+
File.write(output_file, result[:html])
|
151
|
+
|
152
|
+
built_files << result[:output_path]
|
153
|
+
#puts " Built: #{result[:output_path]}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
built_files
|
157
|
+
end
|
158
|
+
|
159
|
+
def build_single_page(page, content_dir)
|
160
|
+
context = PageContext.new(page[:file_path], content_dir)
|
161
|
+
begin
|
162
|
+
source_code = File.read(page[:file_path])
|
163
|
+
|
164
|
+
if source_code.include?('paginate')
|
165
|
+
return build_paginated_pages(page, source_code, context, content_dir)
|
166
|
+
else
|
167
|
+
html = context.instance_eval(source_code, page[:file_path])
|
168
|
+
|
169
|
+
@dependencies[:templates][page[:file_path]] = context.used_dependencies
|
170
|
+
|
171
|
+
return [{ html: html, output_path: page[:output_path] }]
|
172
|
+
end
|
173
|
+
rescue => e
|
174
|
+
#puts " Error building #{page[:output_path]}: #{e.message}"
|
175
|
+
return []
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def build_paginated_pages(page, source_code, context, content_dir)
|
180
|
+
pages_generated = []
|
181
|
+
context.setup_pagination_capture(page)
|
182
|
+
context.instance_eval(source_code, page[:file_path])
|
183
|
+
context.captured_paginations.each do |pagination|
|
184
|
+
pagination[:pages].each_with_index do |page_obj, index|
|
185
|
+
page_context = PageContext.new(page[:file_path], content_dir)
|
186
|
+
page_context.current_pagination_page = page_obj
|
187
|
+
|
188
|
+
html = page_context.instance_eval(pagination[:block_source], page[:file_path])
|
189
|
+
|
190
|
+
output_path = generate_pagination_path(page[:output_path], index + 1)
|
191
|
+
pages_generated << { html: html, output_path: output_path }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
@dependencies[:templates][page[:file_path]] = context.used_dependencies
|
195
|
+
pages_generated
|
196
|
+
end
|
197
|
+
|
198
|
+
def generate_pagination_path(base_path, page_number)
|
199
|
+
if page_number == 1
|
200
|
+
base_path
|
201
|
+
else
|
202
|
+
base_path.sub(/\/index\.html$/, "/page-#{page_number}/index.html")
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def process_assets(content_dir, output_dir)
|
207
|
+
# Copy global assets from public/ to root (no hashing)
|
208
|
+
copy_global_assets(output_dir)
|
209
|
+
|
210
|
+
# Process domain-specific assets with hashing
|
211
|
+
process_domain_assets(content_dir, output_dir)
|
212
|
+
end
|
213
|
+
|
214
|
+
def copy_global_assets(output_dir)
|
215
|
+
['public', 'static'].each do |dir|
|
216
|
+
next unless Dir.exist?(dir)
|
217
|
+
Dir.glob("#{dir}/**/*").each do |file|
|
218
|
+
next if File.directory?(file)
|
219
|
+
|
220
|
+
relative = file.sub("#{dir}/", '')
|
221
|
+
output_file = File.join(output_dir, relative)
|
222
|
+
|
223
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
224
|
+
FileUtils.cp(file, output_file)
|
225
|
+
#puts " Copied global asset: #{relative}"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def process_domain_assets(content_dir, output_dir)
|
231
|
+
# Discover all domains
|
232
|
+
domains = Dir.glob("#{content_dir}/*/").map { |d| File.basename(d) }
|
233
|
+
|
234
|
+
domains.each do |domain|
|
235
|
+
asset_dir = File.join(content_dir, domain, 'assets')
|
236
|
+
next unless Dir.exist?(asset_dir)
|
237
|
+
|
238
|
+
# Determine output path
|
239
|
+
assets_output_dir = if domain == 'default'
|
240
|
+
File.join(output_dir, 'assets')
|
241
|
+
else
|
242
|
+
File.join(output_dir, domain, 'assets')
|
243
|
+
end
|
244
|
+
|
245
|
+
FileUtils.mkdir_p(assets_output_dir)
|
246
|
+
|
247
|
+
# Process each asset file
|
248
|
+
Dir.glob("#{asset_dir}/**/*").each do |file|
|
249
|
+
next if File.directory?(file)
|
250
|
+
|
251
|
+
process_domain_asset(file, asset_dir, assets_output_dir, domain)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def process_domain_asset(file_path, asset_dir, output_dir, domain)
|
257
|
+
# Get relative path within assets directory
|
258
|
+
relative_path = file_path.sub("#{asset_dir}/", '')
|
259
|
+
|
260
|
+
# Read file and generate hash
|
261
|
+
content = File.read(file_path)
|
262
|
+
hash = Digest::MD5.hexdigest(content)[0, 8]
|
263
|
+
|
264
|
+
# Generate hashed filename
|
265
|
+
ext = File.extname(relative_path)
|
266
|
+
base = File.basename(relative_path, ext)
|
267
|
+
dir = File.dirname(relative_path)
|
268
|
+
|
269
|
+
hashed_filename = "#{base}-#{hash}#{ext}"
|
270
|
+
hashed_relative = dir == '.' ? hashed_filename : "#{dir}/#{hashed_filename}"
|
271
|
+
|
272
|
+
# Output file path
|
273
|
+
output_file = File.join(output_dir, hashed_relative)
|
274
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
275
|
+
File.write(output_file, content)
|
276
|
+
|
277
|
+
# Store in manifest
|
278
|
+
manifest_key = "#{domain}/#{relative_path}"
|
279
|
+
manifest_value = if domain == 'default'
|
280
|
+
"/assets/#{hashed_relative}"
|
281
|
+
else
|
282
|
+
"/#{domain}/assets/#{hashed_relative}"
|
283
|
+
end
|
284
|
+
|
285
|
+
@asset_manifest[manifest_key] = manifest_value
|
286
|
+
#puts " Processed asset: #{manifest_key} → #{manifest_value}"
|
287
|
+
end
|
288
|
+
|
289
|
+
def save_dependencies(output_dir)
|
290
|
+
deps_file = File.join(output_dir, '.ssg_dependencies.json')
|
291
|
+
File.write(deps_file, JSON.pretty_generate(@dependencies))
|
292
|
+
end
|
293
|
+
|
294
|
+
def save_asset_manifest(output_dir)
|
295
|
+
manifest_file = File.join(output_dir, '.asset_manifest.json')
|
296
|
+
File.write(manifest_file, JSON.pretty_generate(@asset_manifest))
|
297
|
+
end
|
298
|
+
|
299
|
+
def load_dependencies(output_dir)
|
300
|
+
deps_file = File.join(output_dir, '.ssg_dependencies.json')
|
301
|
+
return unless File.exist?(deps_file)
|
302
|
+
@dependencies = JSON.parse(File.read(deps_file))
|
303
|
+
end
|
304
|
+
|
305
|
+
def load_asset_manifest(output_dir)
|
306
|
+
manifest_file = File.join(output_dir, '.asset_manifest.json')
|
307
|
+
return unless File.exist?(manifest_file)
|
308
|
+
@asset_manifest = JSON.parse(File.read(manifest_file))
|
309
|
+
end
|
310
|
+
|
311
|
+
def find_affected_pages(changed_files)
|
312
|
+
affected = Set.new
|
313
|
+
changed_files.each do |file|
|
314
|
+
if file.include?('/_components/') || file.include?('/_layouts/')
|
315
|
+
template_name = File.basename(file, '.rb')
|
316
|
+
|
317
|
+
@dependencies['templates']&.each do |page_file, deps|
|
318
|
+
if deps['components']&.include?(template_name) ||
|
319
|
+
deps['layouts']&.include?(template_name)
|
320
|
+
affected.add(page_file)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
elsif file.include?('/data/')
|
325
|
+
data_name = File.basename(file, '_data.rb')
|
326
|
+
|
327
|
+
@dependencies['data']&.dig(data_name)&.each do |page_file|
|
328
|
+
affected.add(page_file)
|
329
|
+
end
|
330
|
+
|
331
|
+
elsif file.end_with?('.rb')
|
332
|
+
affected.add(file)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
affected.map { |file| { file_path: file } }.compact
|
336
|
+
end
|
337
|
+
|
338
|
+
def build_error_pages(output_dir, content_dir)
|
339
|
+
return if @error_pages.empty?
|
340
|
+
|
341
|
+
@error_pages.each do |(domain, error_code), error_pages|
|
342
|
+
error_page = error_pages.first # Take the first if multiple
|
343
|
+
|
344
|
+
# Build the error page
|
345
|
+
page_results = build_single_page(error_page, content_dir)
|
346
|
+
next if page_results.empty?
|
347
|
+
|
348
|
+
page_results.each do |result|
|
349
|
+
# Save error pages with their status code names
|
350
|
+
if domain == 'default'
|
351
|
+
output_file = File.join(output_dir, "#{error_code}.html")
|
352
|
+
else
|
353
|
+
output_file = File.join(output_dir, domain, "#{error_code}.html")
|
354
|
+
end
|
355
|
+
|
356
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
357
|
+
File.write(output_file, result[:html])
|
358
|
+
|
359
|
+
#puts " Built error page: #{error_code}.html (#{domain})"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def render_error_page(domain, error_code, context = {})
|
365
|
+
# Find the appropriate error page
|
366
|
+
error_page_info = @error_pages[[domain, error_code]]&.first
|
367
|
+
error_page_info ||= @error_pages[['default', error_code]]&.first
|
368
|
+
|
369
|
+
return nil unless error_page_info
|
370
|
+
|
371
|
+
# Create a context with the error information
|
372
|
+
page_context = PageContext.new(error_page_info[:file_path], File.dirname(error_page_info[:file_path]))
|
373
|
+
|
374
|
+
# Assign context variables as locals (avoiding instance variable caveat)
|
375
|
+
context.each do |key, value|
|
376
|
+
page_context.instance_variable_set("@#{key}", value)
|
377
|
+
end
|
378
|
+
|
379
|
+
begin
|
380
|
+
source_code = File.read(error_page_info[:file_path])
|
381
|
+
page_context.instance_eval(source_code, error_page_info[:file_path])
|
382
|
+
rescue => e
|
383
|
+
#puts " Error rendering error page #{error_code}: #{e.message}"
|
384
|
+
nil
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
class PageContext
|
389
|
+
include Joys::Render::Helpers if defined?(Joys::Render::Helpers)
|
390
|
+
include Joys::Tags if defined?(Joys::Tags)
|
391
|
+
|
392
|
+
attr_reader :used_dependencies, :captured_paginations, :content_dir, :page_file_path
|
393
|
+
attr_accessor :current_pagination_page
|
394
|
+
|
395
|
+
def initialize(page_file_path, content_dir)
|
396
|
+
@page_file_path = page_file_path
|
397
|
+
@content_dir = content_dir
|
398
|
+
@used_dependencies = { 'components' => [], 'layouts' => [], 'data' => [] }
|
399
|
+
@captured_paginations = []
|
400
|
+
@current_pagination_page = nil
|
401
|
+
@bf = String.new # Buffer for HTML output
|
402
|
+
@slots = {} # Slots for layout system
|
403
|
+
|
404
|
+
# Load asset manifest for asset helpers
|
405
|
+
manifest_file = File.join(SSG.output_dir, '.asset_manifest.json')
|
406
|
+
@asset_manifest = if File.exist?(manifest_file)
|
407
|
+
JSON.parse(File.read(manifest_file))
|
408
|
+
else
|
409
|
+
{}
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def setup_pagination_capture(page)
|
414
|
+
@capturing_pagination = true
|
415
|
+
@base_page = page
|
416
|
+
end
|
417
|
+
|
418
|
+
def params
|
419
|
+
{}
|
420
|
+
end
|
421
|
+
|
422
|
+
def session
|
423
|
+
{}
|
424
|
+
end
|
425
|
+
|
426
|
+
def html(&block)
|
427
|
+
if defined?(Joys) && Joys.respond_to?(:html)
|
428
|
+
Joys.html(&block)
|
429
|
+
else
|
430
|
+
yield if block_given?
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
def layout(name, &block)
|
435
|
+
@used_dependencies['layouts'] << name.to_s
|
436
|
+
if defined?(Joys) && Joys.layouts.key?(name)
|
437
|
+
layout_lambda = Joys.layouts[name]
|
438
|
+
# Create a temporary buffer for the layout content
|
439
|
+
old_bf = @bf
|
440
|
+
@bf = String.new
|
441
|
+
result = layout_lambda.call(self, &block)
|
442
|
+
@bf = old_bf
|
443
|
+
@bf << result if result
|
444
|
+
else
|
445
|
+
# Fallback for when Joys layout isn't available
|
446
|
+
yield if block_given?
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
def comp(name, *args)
|
451
|
+
@used_dependencies['components'] << name.to_s
|
452
|
+
if defined?(Joys) && Joys.respond_to?(:comp)
|
453
|
+
Joys.comp(name, *args)
|
454
|
+
else
|
455
|
+
""
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def data(model_name)
|
460
|
+
@used_dependencies['data'] ||= []
|
461
|
+
@used_dependencies['data'] << model_name.to_s
|
462
|
+
|
463
|
+
SSG.dependencies[:data][model_name.to_s] ||= []
|
464
|
+
SSG.dependencies[:data][model_name.to_s] << @page_file_path
|
465
|
+
|
466
|
+
if defined?(Joys::Data)
|
467
|
+
Joys::Data.query(model_name)
|
468
|
+
else
|
469
|
+
MockDataQuery.new
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# Asset helper methods
|
474
|
+
def asset_path(asset_name)
|
475
|
+
domain = extract_domain_from_path(@page_file_path)
|
476
|
+
manifest_key = "#{domain}/#{asset_name}"
|
477
|
+
|
478
|
+
@asset_manifest[manifest_key] || "/assets/#{asset_name}"
|
479
|
+
end
|
480
|
+
|
481
|
+
def asset_url(asset_name, base_url = nil)
|
482
|
+
path = asset_path(asset_name)
|
483
|
+
base_url ||= @site_url || ""
|
484
|
+
base_url.chomp("/") + path
|
485
|
+
end
|
486
|
+
|
487
|
+
def paginate(collection, per_page:, &block)
|
488
|
+
raise ArgumentError, "per_page must be positive" if per_page <= 0
|
489
|
+
|
490
|
+
if collection.respond_to?(:paginate)
|
491
|
+
# Use Joys::Data pagination if available
|
492
|
+
pages = collection.paginate(per_page: per_page)
|
493
|
+
else
|
494
|
+
# Fallback for non-query collections (e.g., plain arrays)
|
495
|
+
items = Array(collection)
|
496
|
+
total_pages = (items.size.to_f / per_page).ceil
|
497
|
+
pages = items.each_slice(per_page).map.with_index do |slice, index|
|
498
|
+
Joys::Data::Page.new(slice, index + 1, total_pages, items.size)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
if @capturing_pagination
|
503
|
+
@captured_paginations << {
|
504
|
+
pages: pages,
|
505
|
+
block_source: extract_block_source(block)
|
506
|
+
}
|
507
|
+
""
|
508
|
+
else
|
509
|
+
if @current_pagination_page
|
510
|
+
yield @current_pagination_page
|
511
|
+
else
|
512
|
+
""
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
private
|
518
|
+
|
519
|
+
def extract_domain_from_path(file_path)
|
520
|
+
# Extract domain from path like content/blog_mysite_com/index.rb -> blog_mysite_com
|
521
|
+
match = file_path.match(%r{#{Regexp.escape(@content_dir)}/([^/]+)/})
|
522
|
+
match ? match[1] : 'default'
|
523
|
+
end
|
524
|
+
|
525
|
+
def create_page_object(items, current_page, total_pages, total_items, base_page)
|
526
|
+
base_path = base_page[:url_path].chomp('/')
|
527
|
+
base_path = '/' if base_path.empty?
|
528
|
+
|
529
|
+
PageObject.new(
|
530
|
+
posts: items, # Using 'posts' for consistency with handoff doc
|
531
|
+
current_page: current_page,
|
532
|
+
total_pages: total_pages,
|
533
|
+
total_items: total_items,
|
534
|
+
prev_page: current_page > 1 ? current_page - 1 : nil,
|
535
|
+
next_page: current_page < total_pages ? current_page + 1 : nil,
|
536
|
+
prev_path: current_page > 1 ? (current_page == 2 ? base_path : "#{base_path}/page-#{current_page - 1}") : nil,
|
537
|
+
next_path: current_page < total_pages ? "#{base_path}/page-#{current_page + 1}" : nil,
|
538
|
+
is_first_page: current_page == 1,
|
539
|
+
is_last_page: current_page == total_pages
|
540
|
+
)
|
541
|
+
end
|
542
|
+
|
543
|
+
def extract_block_source(block)
|
544
|
+
source_location = block.source_location
|
545
|
+
if source_location
|
546
|
+
file, line = source_location
|
547
|
+
lines = File.readlines(file)
|
548
|
+
lines[line - 1] || "yield page"
|
549
|
+
else
|
550
|
+
"yield page"
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
class PageObject
|
556
|
+
def initialize(**attrs)
|
557
|
+
@attrs = attrs
|
558
|
+
end
|
559
|
+
|
560
|
+
def method_missing(method_name, *args)
|
561
|
+
if @attrs.key?(method_name)
|
562
|
+
@attrs[method_name]
|
563
|
+
else
|
564
|
+
super
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
def respond_to_missing?(method_name, include_private = false)
|
569
|
+
@attrs.key?(method_name) || super
|
570
|
+
end
|
571
|
+
|
572
|
+
def to_h
|
573
|
+
@attrs
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
class MockDataQuery
|
578
|
+
def method_missing(method, *args)
|
579
|
+
self
|
580
|
+
end
|
581
|
+
|
582
|
+
def all
|
583
|
+
[]
|
584
|
+
end
|
585
|
+
|
586
|
+
def count
|
587
|
+
0
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
module Joys
|
594
|
+
def self.build_static_site(content_dir, output_dir = 'dist', **options)
|
595
|
+
SSG.build(content_dir, output_dir, **options)
|
596
|
+
end
|
597
|
+
end
|