joys 0.1.2 → 0.1.4
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 +321 -150
- data/joys-0.1.2.gem +0 -0
- data/joys-0.1.3.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 +70 -42
- data/lib/joys/data.rb +477 -0
- data/lib/joys/helpers.rb +36 -9
- data/lib/joys/ssg.rb +597 -0
- data/lib/joys/tags.rb +4 -1
- data/lib/joys/toys.rb +328 -0
- data/lib/joys/version.rb +1 -1
- data/lib/joys.rb +2 -1
- metadata +9 -3
- data/.DS_Store +0 -0
data/lib/joys/data.rb
ADDED
@@ -0,0 +1,477 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# lib/joys/data.rb - Ruby hash-based data querying
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
module Joys
|
8
|
+
module Data
|
9
|
+
# Custom error for data file issues
|
10
|
+
class DataFileError < StandardError; end
|
11
|
+
class ScopeError < StandardError; end
|
12
|
+
class ItemMethodError < StandardError; end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configure(data_path: 'data')
|
16
|
+
@data_path = data_path
|
17
|
+
end
|
18
|
+
|
19
|
+
def define(model_name, &block)
|
20
|
+
context = DefinitionContext.new(model_name, @data_path || 'data')
|
21
|
+
context.instance_eval(&block)
|
22
|
+
|
23
|
+
# Store the loaded data and methods
|
24
|
+
@collections ||= {}
|
25
|
+
@collections[model_name.to_sym] = {
|
26
|
+
data: context.loaded_data,
|
27
|
+
scopes: context.scopes,
|
28
|
+
item_methods: context.item_methods
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def query(model_name)
|
33
|
+
model_name = model_name.to_sym
|
34
|
+
collection = @collections[model_name]
|
35
|
+
|
36
|
+
unless collection
|
37
|
+
raise "Collection #{model_name} not defined. Use Joys::Data.define :#{model_name} to define it."
|
38
|
+
end
|
39
|
+
|
40
|
+
Query.new(
|
41
|
+
model_name,
|
42
|
+
collection[:data],
|
43
|
+
collection[:scopes],
|
44
|
+
collection[:item_methods]
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Development helper methods
|
49
|
+
def reload
|
50
|
+
@collections = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def defined_models
|
54
|
+
(@collections || {}).keys
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class DefinitionContext
|
59
|
+
attr_reader :loaded_data, :scopes, :item_methods
|
60
|
+
|
61
|
+
def initialize(model_name, data_path)
|
62
|
+
@model_name = model_name
|
63
|
+
@data_path = data_path
|
64
|
+
@loaded_data = []
|
65
|
+
@scopes = {}
|
66
|
+
@item_methods = {}
|
67
|
+
end
|
68
|
+
|
69
|
+
def load(pattern)
|
70
|
+
file_pattern = File.join(@data_path, pattern)
|
71
|
+
files = Dir[file_pattern].sort
|
72
|
+
|
73
|
+
if files.empty?
|
74
|
+
raise "No files found matching pattern: #{file_pattern}"
|
75
|
+
end
|
76
|
+
|
77
|
+
@loaded_data = files.map do |file|
|
78
|
+
begin
|
79
|
+
# Each file should return a hash - evaluate in clean context
|
80
|
+
hash_data = eval(File.read(file), TOPLEVEL_BINDING, file)
|
81
|
+
unless hash_data.is_a?(::Hash)
|
82
|
+
raise DataFileError, "#{file} should return a Hash, but returned #{hash_data.class}.\n\nMake sure your data file ends with a hash like:\n{\n title: \"My Post\",\n content: \"...\"\n}"
|
83
|
+
end
|
84
|
+
hash_data.freeze # Make immutable
|
85
|
+
rescue DataFileError
|
86
|
+
raise # Re-raise our custom errors as-is
|
87
|
+
rescue SyntaxError => e
|
88
|
+
raise DataFileError, "#{file} has a syntax error:\n#{e.message}\n\nCheck for missing commas, quotes, or brackets."
|
89
|
+
rescue => e
|
90
|
+
raise DataFileError, "#{file} couldn't be loaded:\n#{e.message}\n\nMake sure the file contains valid Ruby code that returns a Hash."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def from_array(array)
|
96
|
+
unless array.is_a?(Array)
|
97
|
+
raise DataFileError, "from_array() expects an Array of hashes, but got #{array.class}.\n\nExample:\nfrom_array([\n { title: \"First Post\", content: \"...\" },\n { title: \"Second Post\", content: \"...\" }\n])"
|
98
|
+
end
|
99
|
+
|
100
|
+
@loaded_data = array.map.with_index do |item, index|
|
101
|
+
unless item.is_a?(::Hash)
|
102
|
+
raise DataFileError, "from_array() array item #{index + 1} should be a Hash, but got #{item.class}.\n\nEach item in the array should look like:\n{ title: \"Post Title\", content: \"Post content\" }"
|
103
|
+
end
|
104
|
+
item.freeze # Make immutable
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def scope(methods_hash)
|
109
|
+
methods_hash.each do |name, lambda_proc|
|
110
|
+
unless lambda_proc.respond_to?(:call)
|
111
|
+
raise "Scope #{name} must be callable (lambda or proc)"
|
112
|
+
end
|
113
|
+
@scopes[name.to_sym] = lambda_proc
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def item(methods_hash)
|
118
|
+
methods_hash.each do |name, lambda_proc|
|
119
|
+
unless lambda_proc.respond_to?(:call)
|
120
|
+
raise "Item method #{name} must be callable (lambda or proc)"
|
121
|
+
end
|
122
|
+
@item_methods[name.to_sym] = lambda_proc
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class Query
|
128
|
+
include Enumerable
|
129
|
+
|
130
|
+
def initialize(model_name, data, scopes, item_methods, conditions: [], order_field: nil, order_desc: false, limit_count: nil, offset_count: nil)
|
131
|
+
@model_name = model_name
|
132
|
+
@data = data
|
133
|
+
@scopes = scopes
|
134
|
+
@item_methods = item_methods
|
135
|
+
@conditions = conditions
|
136
|
+
@order_field = order_field
|
137
|
+
@order_desc = order_desc
|
138
|
+
@limit_count = limit_count
|
139
|
+
@offset_count = offset_count
|
140
|
+
|
141
|
+
# Dynamically define scope methods with better error context
|
142
|
+
@scopes.each do |scope_name, scope_lambda|
|
143
|
+
define_singleton_method(scope_name) do |*args|
|
144
|
+
begin
|
145
|
+
instance_exec(*args, &scope_lambda)
|
146
|
+
rescue => e
|
147
|
+
raise "Error in scope :#{scope_name} for model :#{@model_name}: #{e.message}\n #{e.backtrace&.first}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Core query methods
|
154
|
+
def where(conditions)
|
155
|
+
Query.new(@model_name, @data, @scopes, @item_methods,
|
156
|
+
conditions: @conditions + [conditions],
|
157
|
+
order_field: @order_field,
|
158
|
+
order_desc: @order_desc,
|
159
|
+
limit_count: @limit_count,
|
160
|
+
offset_count: @offset_count)
|
161
|
+
end
|
162
|
+
|
163
|
+
def order(field, desc: false)
|
164
|
+
Query.new(@model_name, @data, @scopes, @item_methods,
|
165
|
+
conditions: @conditions,
|
166
|
+
order_field: field,
|
167
|
+
order_desc: desc,
|
168
|
+
limit_count: @limit_count,
|
169
|
+
offset_count: @offset_count)
|
170
|
+
end
|
171
|
+
|
172
|
+
def limit(count)
|
173
|
+
Query.new(@model_name, @data, @scopes, @item_methods,
|
174
|
+
conditions: @conditions,
|
175
|
+
order_field: @order_field,
|
176
|
+
order_desc: @order_desc,
|
177
|
+
limit_count: count,
|
178
|
+
offset_count: @offset_count)
|
179
|
+
end
|
180
|
+
|
181
|
+
def offset(count)
|
182
|
+
Query.new(@model_name, @data, @scopes, @item_methods,
|
183
|
+
conditions: @conditions,
|
184
|
+
order_field: @order_field,
|
185
|
+
order_desc: @order_desc,
|
186
|
+
limit_count: @limit_count,
|
187
|
+
offset_count: count)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Terminal methods
|
191
|
+
def all
|
192
|
+
apply_filters.map { |hash| wrap_instance(hash) }
|
193
|
+
end
|
194
|
+
|
195
|
+
def first
|
196
|
+
result = apply_filters.first
|
197
|
+
result ? wrap_instance(result) : nil
|
198
|
+
end
|
199
|
+
|
200
|
+
def last
|
201
|
+
result = apply_filters.last
|
202
|
+
result ? wrap_instance(result) : nil
|
203
|
+
end
|
204
|
+
|
205
|
+
def count
|
206
|
+
apply_filters.length
|
207
|
+
end
|
208
|
+
|
209
|
+
def find_by(conditions)
|
210
|
+
where(conditions).first
|
211
|
+
end
|
212
|
+
|
213
|
+
# Enumerable support
|
214
|
+
def each(&block)
|
215
|
+
all.each(&block)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Pagination support
|
219
|
+
def paginate(per_page:)
|
220
|
+
raise ArgumentError, "per_page must be positive" if per_page <= 0
|
221
|
+
total_items = count
|
222
|
+
return [Page.new([], 1, 1, total_items)] if total_items == 0
|
223
|
+
total_pages = (total_items.to_f / per_page).ceil
|
224
|
+
(1..total_pages).map do |page_num|
|
225
|
+
offset_val = (page_num - 1) * per_page
|
226
|
+
# Respect existing limits - don't override them
|
227
|
+
items_per_page = if @limit_count && (@limit_count < per_page)
|
228
|
+
# If existing limit is smaller than per_page, use the existing limit
|
229
|
+
remaining_items = [@limit_count - offset_val, 0].max
|
230
|
+
[remaining_items, per_page].min
|
231
|
+
else
|
232
|
+
per_page
|
233
|
+
end
|
234
|
+
items = self.offset(offset_val).limit(items_per_page).all
|
235
|
+
Page.new(items, page_num, total_pages, total_items)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def apply_filters
|
242
|
+
result = @data.dup
|
243
|
+
|
244
|
+
# Apply conditions
|
245
|
+
@conditions.each do |condition|
|
246
|
+
result = result.select do |item|
|
247
|
+
condition.all? do |field, value|
|
248
|
+
evaluate_condition(item, field.to_sym, value) # Normalize field to symbol
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Apply ordering
|
254
|
+
if @order_field
|
255
|
+
# Separate nil values to always sort them to the end
|
256
|
+
nil_items = result.select { |item| item[@order_field.to_sym].nil? }
|
257
|
+
non_nil_items = result.reject { |item| item[@order_field.to_sym].nil? }
|
258
|
+
|
259
|
+
# Sort non-nil items with proper type handling
|
260
|
+
sorted_non_nil = non_nil_items.sort_by do |item|
|
261
|
+
value = item[@order_field.to_sym]
|
262
|
+
# Handle different types properly for sorting
|
263
|
+
case value
|
264
|
+
when Numeric
|
265
|
+
[0, value] # Numeric values first, keep as numeric
|
266
|
+
when String
|
267
|
+
[1, value] # Strings second, keep as string
|
268
|
+
when Date, Time
|
269
|
+
[2, value] # Dates third, keep as date
|
270
|
+
else
|
271
|
+
[3, value.to_s] # Everything else as string
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Combine: non-nil items + nil items (nil always at end)
|
276
|
+
result = sorted_non_nil + nil_items
|
277
|
+
|
278
|
+
# For descending, reverse only the non-nil portion
|
279
|
+
if @order_desc
|
280
|
+
result = sorted_non_nil.reverse + nil_items
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Apply offset
|
285
|
+
if @offset_count && @offset_count > 0
|
286
|
+
result = result[@offset_count..-1] || []
|
287
|
+
end
|
288
|
+
|
289
|
+
# Apply limit
|
290
|
+
if @limit_count
|
291
|
+
result = result[0, @limit_count] || []
|
292
|
+
end
|
293
|
+
|
294
|
+
result
|
295
|
+
end
|
296
|
+
|
297
|
+
def evaluate_condition(item, field, value)
|
298
|
+
field_value = item[field]
|
299
|
+
|
300
|
+
case value
|
301
|
+
when ::Hash
|
302
|
+
if value.key?(:contains)
|
303
|
+
field_value.to_s.include?(value[:contains].to_s)
|
304
|
+
elsif value.key?(:starts_with)
|
305
|
+
field_value.to_s.start_with?(value[:starts_with].to_s)
|
306
|
+
elsif value.key?(:ends_with)
|
307
|
+
field_value.to_s.end_with?(value[:ends_with].to_s)
|
308
|
+
elsif value.key?(:greater_than)
|
309
|
+
field_value && field_value > value[:greater_than]
|
310
|
+
elsif value.key?(:less_than)
|
311
|
+
field_value && field_value < value[:less_than]
|
312
|
+
elsif value.key?(:greater_than_or_equal)
|
313
|
+
field_value && field_value >= value[:greater_than_or_equal]
|
314
|
+
elsif value.key?(:less_than_or_equal)
|
315
|
+
field_value && field_value <= value[:less_than_or_equal]
|
316
|
+
elsif value.key?(:from) && value.key?(:to)
|
317
|
+
field_value && field_value >= value[:from] && field_value <= value[:to]
|
318
|
+
elsif value.key?(:from)
|
319
|
+
field_value && field_value >= value[:from]
|
320
|
+
elsif value.key?(:to)
|
321
|
+
field_value && field_value <= value[:to]
|
322
|
+
elsif value.key?(:in)
|
323
|
+
Array(value[:in]).include?(field_value)
|
324
|
+
elsif value.key?(:not_in)
|
325
|
+
!Array(value[:not_in]).include?(field_value)
|
326
|
+
elsif value.key?(:not)
|
327
|
+
field_value != value[:not]
|
328
|
+
elsif value.key?(:exists)
|
329
|
+
value[:exists] ? !field_value.nil? : field_value.nil?
|
330
|
+
elsif value.key?(:empty)
|
331
|
+
if value[:empty]
|
332
|
+
field_value.nil? || field_value == '' || (field_value.respond_to?(:empty?) && field_value.empty?)
|
333
|
+
else
|
334
|
+
!field_value.nil? && field_value != '' && !(field_value.respond_to?(:empty?) && field_value.empty?)
|
335
|
+
end
|
336
|
+
else
|
337
|
+
field_value == value
|
338
|
+
end
|
339
|
+
when Array
|
340
|
+
if field_value.is_a?(Array)
|
341
|
+
# For array fields, check if they contain the same elements
|
342
|
+
field_value == value
|
343
|
+
else
|
344
|
+
# For non-array fields, check if field_value is in the array
|
345
|
+
value.include?(field_value)
|
346
|
+
end
|
347
|
+
when Range
|
348
|
+
value.include?(field_value)
|
349
|
+
else
|
350
|
+
field_value == value
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def wrap_instance(hash)
|
355
|
+
InstanceWrapper.new(hash, @item_methods)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
class Page
|
360
|
+
attr_reader :items, :current_page, :total_pages, :total_items,
|
361
|
+
:prev_page, :next_page, :is_first_page, :is_last_page
|
362
|
+
|
363
|
+
def initialize(items, current_page, total_pages, total_items)
|
364
|
+
@items = items
|
365
|
+
@current_page = current_page
|
366
|
+
@total_pages = total_pages
|
367
|
+
@total_items = total_items
|
368
|
+
@prev_page = current_page > 1 ? current_page - 1 : nil
|
369
|
+
@next_page = current_page < total_pages ? current_page + 1 : nil
|
370
|
+
@is_first_page = current_page == 1
|
371
|
+
@is_last_page = current_page == total_pages
|
372
|
+
end
|
373
|
+
|
374
|
+
# Aliases for consistency with your SSG (e.g., 'posts' instead of 'items')
|
375
|
+
alias_method :posts, :items
|
376
|
+
|
377
|
+
def to_h
|
378
|
+
{
|
379
|
+
items: @items,
|
380
|
+
current_page: @current_page,
|
381
|
+
total_pages: @total_pages,
|
382
|
+
total_items: @total_items,
|
383
|
+
prev_page: @prev_page,
|
384
|
+
next_page: @next_page,
|
385
|
+
is_first_page: @is_first_page,
|
386
|
+
is_last_page: @is_last_page
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
class InstanceWrapper
|
392
|
+
def initialize(hash, item_methods)
|
393
|
+
@hash = hash
|
394
|
+
@item_methods = item_methods
|
395
|
+
@memoized = {}
|
396
|
+
|
397
|
+
# Dynamically define item methods with better error context
|
398
|
+
@item_methods.each do |method_name, method_lambda|
|
399
|
+
define_singleton_method(method_name) do |*args|
|
400
|
+
# Memoize results to avoid recomputation
|
401
|
+
cache_key = [method_name, args]
|
402
|
+
|
403
|
+
unless @memoized.key?(cache_key)
|
404
|
+
begin
|
405
|
+
@memoized[cache_key] = instance_exec(*args, &method_lambda)
|
406
|
+
rescue => e
|
407
|
+
raise "Error in item method :#{method_name}: #{e.message}\n #{e.backtrace&.first}"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
@memoized[cache_key]
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# Hash-style access with symbol normalization
|
417
|
+
def [](key)
|
418
|
+
# Try symbol first, then string, then original key
|
419
|
+
@hash[key.to_sym] || @hash[key]
|
420
|
+
end
|
421
|
+
|
422
|
+
def []=(key, value)
|
423
|
+
raise "Joys::Data collections are immutable. Cannot assign #{key} = #{value}"
|
424
|
+
end
|
425
|
+
|
426
|
+
def has_key?(key)
|
427
|
+
@hash.has_key?(key.to_sym) || @hash.has_key?(key.to_s)
|
428
|
+
end
|
429
|
+
|
430
|
+
def include?(key)
|
431
|
+
@hash.include?(key.to_sym) || @hash.include?(key.to_s)
|
432
|
+
end
|
433
|
+
|
434
|
+
def key?(key)
|
435
|
+
@hash.key?(key.to_sym) || @hash.key?(key.to_s)
|
436
|
+
end
|
437
|
+
|
438
|
+
def keys
|
439
|
+
@hash.keys
|
440
|
+
end
|
441
|
+
|
442
|
+
def values
|
443
|
+
@hash.values
|
444
|
+
end
|
445
|
+
|
446
|
+
def to_h
|
447
|
+
@hash
|
448
|
+
end
|
449
|
+
|
450
|
+
def to_hash
|
451
|
+
@hash
|
452
|
+
end
|
453
|
+
|
454
|
+
# Make it behave like a hash for other operations
|
455
|
+
def method_missing(method, *args, &block)
|
456
|
+
if @hash.respond_to?(method)
|
457
|
+
@hash.send(method, *args, &block)
|
458
|
+
else
|
459
|
+
super
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def respond_to_missing?(method, include_private = false)
|
464
|
+
@hash.respond_to?(method, include_private) || super
|
465
|
+
end
|
466
|
+
|
467
|
+
def inspect
|
468
|
+
"#<Joys::Data::Instance #{@hash.inspect}>"
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# Global helper method for cleaner syntax
|
475
|
+
def data(model_name)
|
476
|
+
Joys::Data.query(model_name)
|
477
|
+
end
|
data/lib/joys/helpers.rb
CHANGED
@@ -5,6 +5,7 @@ module Joys
|
|
5
5
|
module Helpers
|
6
6
|
def txt(content); @bf << CGI.escapeHTML(content.to_s); nil; end
|
7
7
|
def raw(content); @bf << content.to_s; nil; end
|
8
|
+
def _(content); @bf << content.to_s; nil; end
|
8
9
|
def push(name, &block)
|
9
10
|
old_buffer = @bf;@slots ||= {}
|
10
11
|
@bf = String.new;instance_eval(&block)
|
@@ -14,15 +15,26 @@ module Joys
|
|
14
15
|
def pull(name = :main)
|
15
16
|
@slots ||= {};@bf << @slots[name].to_s if @slots[name];nil
|
16
17
|
end
|
17
|
-
def pull_styles
|
18
|
+
def pull_styles
|
19
|
+
@bf << "<!--STYLES-->"
|
20
|
+
nil
|
21
|
+
end
|
18
22
|
def pull_external_styles;@bf << "<!--EXTERNAL_STYLES-->";nil;end
|
19
23
|
|
20
|
-
def comp(name, *args, **kwargs, &block)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
def comp(name, *args, **kwargs, &block)
|
25
|
+
comp_name = "comp_#{name}"
|
26
|
+
@used_components ||= Set.new
|
27
|
+
@used_components.add(comp_name) # 1. Register the component being called.
|
28
|
+
|
29
|
+
# 2. Call the component's render method. This now returns our hash.
|
30
|
+
result = Joys.send(comp_name, *args, **kwargs, &block)
|
31
|
+
|
32
|
+
# 3. CRITICAL FIX: Merge the nested dependencies from the result into the current context.
|
33
|
+
@used_components.merge(result[:components]) if result[:components]
|
34
|
+
|
35
|
+
# 4. Render only the HTML part into the buffer.
|
36
|
+
raw result[:html]
|
37
|
+
end
|
26
38
|
def layout(name, &block)
|
27
39
|
layout_lambda = Joys.layouts[name]
|
28
40
|
raise ArgumentError, "Layout `#{name}` not registered" unless layout_lambda
|
@@ -32,25 +44,35 @@ end
|
|
32
44
|
context_name = Joys.current_component
|
33
45
|
context_name ||= Joys.current_page
|
34
46
|
context_name ||= @current_page
|
47
|
+
|
35
48
|
return nil unless context_name
|
36
|
-
return nil if Joys.compiled_styles[context_name]
|
49
|
+
return nil if Joys.compiled_styles[context_name] && !Joys::Config.dev? # Check dev mode
|
50
|
+
|
37
51
|
@style_base_css = []
|
38
52
|
@style_media_queries = {}
|
39
53
|
@style_scoped = scoped
|
40
54
|
instance_eval(&block)
|
55
|
+
|
41
56
|
Joys::Styles.compile_component_styles(
|
42
57
|
context_name,
|
43
58
|
@style_base_css,
|
44
59
|
@style_media_queries,
|
45
60
|
@style_scoped
|
46
61
|
)
|
47
|
-
|
62
|
+
|
63
|
+
if context_name&.start_with?('page_') || context_name&.start_with?('comp_')
|
48
64
|
@used_components ||= Set.new
|
49
65
|
@used_components.add(context_name)
|
50
66
|
end
|
51
67
|
@style_base_css = @style_media_queries = @style_scoped = nil;nil
|
52
68
|
end
|
53
69
|
def doctype;raw "<!doctype html>";end
|
70
|
+
def load_css(*args)
|
71
|
+
args.map do |name|
|
72
|
+
@style_base_css&.push(Joys.css_registry[name]||raise("CSS part not found: #{name}"))
|
73
|
+
end
|
74
|
+
nil
|
75
|
+
end
|
54
76
|
def css(content);@style_base_css&.push(content);nil;end
|
55
77
|
def media_max(breakpoint, content)
|
56
78
|
return nil unless @style_media_queries
|
@@ -106,6 +128,11 @@ end
|
|
106
128
|
@style_media_queries[key] ||= []
|
107
129
|
@style_media_queries[key] << content;nil
|
108
130
|
end
|
131
|
+
def cycle(i, *choices)
|
132
|
+
choices[i % choices.length]
|
133
|
+
end
|
134
|
+
|
135
|
+
|
109
136
|
end
|
110
137
|
end
|
111
138
|
end
|