daisyui 2.0.2 → 2.1.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 +4 -4
- data/CHANGELOG.md +140 -21
- data/README.md +156 -20
- data/data/daisyui/tailwind_manifest.json +2278 -0
- data/exe/daisyui-tailwind +6 -0
- data/lib/daisy_ui/base.rb +45 -9
- data/lib/daisy_ui/class_merge/configuration.rb +18 -3
- data/lib/daisy_ui/class_merge/daisy_groups.rb +8 -5
- data/lib/daisy_ui/class_merge/tailwind_config.rb +8 -18
- data/lib/daisy_ui/class_merge/validators.rb +4 -0
- data/lib/daisy_ui/class_merge.rb +23 -12
- data/lib/daisy_ui/configurable/report_log.rb +36 -0
- data/lib/daisy_ui/configurable.rb +81 -16
- data/lib/daisy_ui/engine.rb +7 -0
- data/lib/daisy_ui/mcp_server.rb +1 -0
- data/lib/daisy_ui/shareable.rb +56 -0
- data/lib/daisy_ui/store.rb +71 -0
- data/lib/daisy_ui/tailwind/candidate.rb +44 -0
- data/lib/daisy_ui/tailwind/command.rb +85 -0
- data/lib/daisy_ui/tailwind/lexer.rb +31 -0
- data/lib/daisy_ui/tailwind/manifest.rb +59 -0
- data/lib/daisy_ui/tailwind/manifest_builder.rb +86 -0
- data/lib/daisy_ui/tailwind/sources.rb +103 -0
- data/lib/daisy_ui/tailwind/unknown_component_error.rb +17 -0
- data/lib/daisy_ui/tailwind/usage.rb +72 -0
- data/lib/daisy_ui/tailwind.rb +47 -0
- data/lib/daisy_ui/updated_at.rb +1 -1
- data/lib/daisy_ui/version.rb +1 -1
- metadata +16 -1
data/lib/daisy_ui/base.rb
CHANGED
|
@@ -118,14 +118,37 @@ module DaisyUI
|
|
|
118
118
|
error: "bg-error text-error-content"
|
|
119
119
|
}.freeze
|
|
120
120
|
|
|
121
|
+
# Component registries: written by the class body (register_modifiers,
|
|
122
|
+
# register_flags, `self.component_class =`) and by `inherited`, on the main
|
|
123
|
+
# Ractor while the class loads; rendering only reads them. Every writer
|
|
124
|
+
# assigns a new value instead of mutating the old one and freezes it as it
|
|
125
|
+
# writes (Shareable.share), so a non-main Ractor can read the registries of
|
|
126
|
+
# a component that loaded after boot too, and a later register_* still
|
|
127
|
+
# works.
|
|
121
128
|
class << self
|
|
122
|
-
|
|
123
|
-
|
|
129
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable -- see above
|
|
130
|
+
attr_reader :modifiers
|
|
131
|
+
|
|
132
|
+
def modifiers=(modifiers)
|
|
133
|
+
@modifiers = Shareable.share(modifiers)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def modifier_groups=(groups)
|
|
137
|
+
@modifier_groups = Shareable.share(groups)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def flags=(flags)
|
|
141
|
+
@flags = Shareable.share(flags)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def component_class=(component_class)
|
|
145
|
+
@component_class = Shareable.share(component_class)
|
|
146
|
+
end
|
|
124
147
|
|
|
125
148
|
# Positional symbols a component handles itself (e.g. Dropdown's
|
|
126
149
|
# :popover): accepted as modifiers, but they emit no class.
|
|
127
150
|
def flags
|
|
128
|
-
@flags
|
|
151
|
+
@flags || []
|
|
129
152
|
end
|
|
130
153
|
|
|
131
154
|
def register_flags(*names)
|
|
@@ -135,20 +158,24 @@ module DaisyUI
|
|
|
135
158
|
# { group => [modifier, ...] }: modifiers in one group are alternatives
|
|
136
159
|
# (sizes, colors, ...), so ClassMerge keeps only the last one.
|
|
137
160
|
def modifier_groups
|
|
138
|
-
@modifier_groups
|
|
161
|
+
@modifier_groups || {}
|
|
139
162
|
end
|
|
140
163
|
|
|
164
|
+
# Derived from the class name on first use, the same value every time.
|
|
165
|
+
# Remembered on the main Ractor only (DaisyUI.finalize! derives it at
|
|
166
|
+
# boot); a non-main Ractor cannot set a class ivar, so it derives it.
|
|
141
167
|
def component_class
|
|
142
168
|
return @component_class if instance_variable_defined?(:@component_class)
|
|
143
169
|
|
|
144
|
-
|
|
170
|
+
derived = begin
|
|
145
171
|
# Convert "DaisyUI::Button" -> "button"
|
|
146
172
|
class_name = name.split("::").last # demodulize
|
|
147
173
|
class_name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
148
174
|
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
149
175
|
.tr("_", "-")
|
|
150
|
-
.downcase
|
|
176
|
+
.downcase.freeze
|
|
151
177
|
end
|
|
178
|
+
Store.main_ractor? ? (@component_class = derived) : derived
|
|
152
179
|
end
|
|
153
180
|
|
|
154
181
|
def inherited(subclass)
|
|
@@ -161,6 +188,7 @@ module DaisyUI
|
|
|
161
188
|
|
|
162
189
|
subclass.component_class = @component_class
|
|
163
190
|
end
|
|
191
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
164
192
|
|
|
165
193
|
# Registers modifiers. A Hash value declares a group of alternatives:
|
|
166
194
|
#
|
|
@@ -178,15 +206,23 @@ module DaisyUI
|
|
|
178
206
|
end
|
|
179
207
|
end
|
|
180
208
|
|
|
181
|
-
modifier_groups.
|
|
209
|
+
groups = modifier_groups.transform_values { |members| members.reject { |member| flat.key?(member) } }
|
|
182
210
|
mods.each do |group, value|
|
|
183
|
-
(
|
|
211
|
+
groups[group] = (groups[group] || []) + value.keys if value.is_a?(Hash)
|
|
184
212
|
end
|
|
185
|
-
modifier_groups.delete_if { |_group, members| members.empty? }
|
|
186
213
|
|
|
214
|
+
self.modifier_groups = groups.reject { |_group, members| members.empty? }
|
|
187
215
|
self.modifiers = (modifiers || {}).merge(flat)
|
|
188
216
|
ClassMerge.reset!
|
|
189
217
|
end
|
|
218
|
+
|
|
219
|
+
# @api private Called by DaisyUI.finalize! for every component: derives
|
|
220
|
+
# component_class on the main Ractor and makes sure every registry is
|
|
221
|
+
# frozen for non-main Ractors.
|
|
222
|
+
def finalize!
|
|
223
|
+
[component_class, modifiers, modifier_groups, flags].each { |registry| Shareable.share(registry) }
|
|
224
|
+
self
|
|
225
|
+
end
|
|
190
226
|
end
|
|
191
227
|
|
|
192
228
|
def initialize(*modifiers, as: :div, id: nil, **options)
|
|
@@ -24,6 +24,20 @@ module DaisyUI
|
|
|
24
24
|
@conflicts = {}
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def initialize_copy(source)
|
|
28
|
+
super
|
|
29
|
+
@theme = @theme.transform_values(&:dup)
|
|
30
|
+
@class_groups = @class_groups.transform_values(&:dup)
|
|
31
|
+
@conflicts = @conflicts.transform_values(&:dup)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api private Called by DaisyUI.finalize!. A raw class group's
|
|
35
|
+
# validator that closes over app state is kept as it is.
|
|
36
|
+
def finalize!
|
|
37
|
+
[@theme, @class_groups, @conflicts].each { |settings| Shareable.share(settings) }
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
# Registers utilities that behave like an existing one:
|
|
28
42
|
# `utility("text-display", like: "text-lg")` makes `text-display` a font
|
|
29
43
|
# size, so it replaces `text-sm` and sits next to `text-error`.
|
|
@@ -57,11 +71,12 @@ module DaisyUI
|
|
|
57
71
|
append(@conflicts, conflicts)
|
|
58
72
|
end
|
|
59
73
|
|
|
60
|
-
# @api private The config passed to Merger.new, including the daisyUI
|
|
61
|
-
|
|
74
|
+
# @api private The config passed to Merger.new, including the daisyUI
|
|
75
|
+
# groups of +configuration+ (the DaisyUI configuration holding this one).
|
|
76
|
+
def merger_config(configuration = DaisyUI.configuration)
|
|
62
77
|
{
|
|
63
78
|
theme: @theme,
|
|
64
|
-
class_groups: DaisyGroups.build.merge(@class_groups) { |_id, existing, added| existing + added },
|
|
79
|
+
class_groups: DaisyGroups.build(configuration).merge(@class_groups) { |_id, existing, added| existing + added },
|
|
65
80
|
conflicting_class_groups: @conflicts
|
|
66
81
|
}
|
|
67
82
|
end
|
|
@@ -18,8 +18,9 @@ module DaisyUI
|
|
|
18
18
|
# @api private
|
|
19
19
|
module DaisyGroups
|
|
20
20
|
class << self
|
|
21
|
-
# Class groups for the merger: `{ "daisy:group:btn-size" => ["btn-xs", ...], ... }
|
|
22
|
-
|
|
21
|
+
# Class groups for the merger: `{ "daisy:group:btn-size" => ["btn-xs", ...], ... }`,
|
|
22
|
+
# from one configuration's prefix and modifiers.
|
|
23
|
+
def build(configuration = DaisyUI.configuration, prefix: configuration.prefix)
|
|
23
24
|
components = self.components
|
|
24
25
|
groups = {}
|
|
25
26
|
|
|
@@ -37,7 +38,7 @@ module DaisyUI
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# Configured modifiers join the component's own group of that name.
|
|
40
|
-
|
|
41
|
+
configuration.modifiers.groups.each do |(component, group), classes|
|
|
41
42
|
owner = component ? component.component_class || component.name : "global"
|
|
42
43
|
add(groups, "daisy:group:#{owner}-#{group}", classes.flat_map(&:split), prefix)
|
|
43
44
|
end
|
|
@@ -51,9 +52,11 @@ module DaisyUI
|
|
|
51
52
|
|
|
52
53
|
# Every named DaisyUI::Base subclass: the gem's components (eager
|
|
53
54
|
# loaded) and any app component loaded so far. An app component that
|
|
54
|
-
# loads later resets the merger through `register_modifiers`.
|
|
55
|
+
# loads later resets the merger through `register_modifiers`. Only the
|
|
56
|
+
# main Ractor can eager-load; DaisyUI.finalize! did it there before
|
|
57
|
+
# any other Ractor builds a merger.
|
|
55
58
|
def components
|
|
56
|
-
Zeitwerk::Loader.eager_load_namespace(DaisyUI)
|
|
59
|
+
Zeitwerk::Loader.eager_load_namespace(DaisyUI) if Store.main_ractor?
|
|
57
60
|
descendants(DaisyUI::Base).select(&:name)
|
|
58
61
|
end
|
|
59
62
|
|
|
@@ -8,21 +8,6 @@ module DaisyUI
|
|
|
8
8
|
module TailwindConfig
|
|
9
9
|
include Validators
|
|
10
10
|
|
|
11
|
-
class << self
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
# Recursively freezes nested Hash/Array structures so shared config
|
|
15
|
-
# constants can't be mutated in place. Keys are strings/symbols and are
|
|
16
|
-
# already immutable, so only values need walking.
|
|
17
|
-
def deep_freeze(object)
|
|
18
|
-
case object
|
|
19
|
-
when Hash then object.each_value { |value| deep_freeze(value) }
|
|
20
|
-
when Array then object.each { |value| deep_freeze(value) }
|
|
21
|
-
end
|
|
22
|
-
object.freeze
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
11
|
THEME_KEYS = %w[
|
|
27
12
|
color
|
|
28
13
|
font
|
|
@@ -46,8 +31,8 @@ module DaisyUI
|
|
|
46
31
|
].freeze
|
|
47
32
|
THEME_KEYS.each do |key|
|
|
48
33
|
const_set("THEME_#{key.upcase.tr('-', '_')}", lambda { |config|
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
config[:theme].fetch(key, nil)
|
|
35
|
+
}
|
|
51
36
|
)
|
|
52
37
|
end
|
|
53
38
|
|
|
@@ -2465,7 +2450,12 @@ module DaisyUI
|
|
|
2465
2450
|
"selection"
|
|
2466
2451
|
]
|
|
2467
2452
|
}.freeze
|
|
2468
|
-
|
|
2453
|
+
|
|
2454
|
+
# Every constant above, DEFAULTS deeply, is made Ractor-shareable so a
|
|
2455
|
+
# non-main Ractor can build its own merger from them: the scale and theme
|
|
2456
|
+
# lambdas are capture-free (THEME_* only reads its frozen key) and their
|
|
2457
|
+
# self is this module.
|
|
2458
|
+
constants(false).each { |name| Shareable.make(const_get(name)) }
|
|
2469
2459
|
|
|
2470
2460
|
# Extends DEFAULTS with incoming_config without mutating either.
|
|
2471
2461
|
# Theme scales are appended. Unlike upstream, `class_groups` and
|
|
@@ -197,6 +197,10 @@ module DaisyUI
|
|
|
197
197
|
IS_LABEL_SHADOW = lambda { |label|
|
|
198
198
|
label == "shadow"
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
# Ractor-shareable, so a non-main Ractor can read them: every validator
|
|
202
|
+
# is a capture-free lambda whose self is this module.
|
|
203
|
+
constants(false).each { |name| Shareable.make(const_get(name)) }
|
|
200
204
|
end
|
|
201
205
|
end
|
|
202
206
|
end
|
data/lib/daisy_ui/class_merge.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "monitor"
|
|
4
|
-
|
|
5
3
|
module DaisyUI
|
|
6
4
|
# Merges CSS class lists so that later classes win over earlier conflicting
|
|
7
5
|
# ones, for Tailwind utilities and daisyUI modifiers alike:
|
|
@@ -16,10 +14,10 @@ module DaisyUI
|
|
|
16
14
|
module ClassMerge
|
|
17
15
|
TAILWIND_MERGE_VERSION = "1.5.6"
|
|
18
16
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
# The merger (with its LRU) and the Tailwind class groups live in
|
|
18
|
+
# DaisyUI::Store: one per Ractor, built on first use under the store's
|
|
19
|
+
# reentrant lock, so a reset! triggered while the merger is being built
|
|
20
|
+
# (e.g. a component file configuring DaisyUI as it loads) cannot deadlock.
|
|
23
21
|
class << self
|
|
24
22
|
# Accepts strings, nested arrays, nil and false (dropped, as Phlex does
|
|
25
23
|
# for `class:`). Returns a frozen String, "" when empty.
|
|
@@ -27,20 +25,33 @@ module DaisyUI
|
|
|
27
25
|
merger.merge(parts.flatten.select(&:itself).join(" "))
|
|
28
26
|
end
|
|
29
27
|
|
|
28
|
+
# Each Ractor's merger remembers the configuration it was built from
|
|
29
|
+
# and is rebuilt once DaisyUI.configure publishes another, also in a
|
|
30
|
+
# Ractor where reset! never ran.
|
|
30
31
|
def merger
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
configuration = DaisyUI.configuration
|
|
33
|
+
built_for, merger = Store.fetch(:daisy_ui_class_merger) { build_merger(configuration) }
|
|
34
|
+
return merger if built_for.equal?(configuration)
|
|
35
|
+
|
|
36
|
+
Store.delete(:daisy_ui_class_merger)
|
|
37
|
+
Store.fetch(:daisy_ui_class_merger) { build_merger(configuration) }.last
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
# @api private The stock Tailwind class groups, for classifying tokens.
|
|
37
41
|
def tailwind_utils
|
|
38
|
-
|
|
42
|
+
Store.fetch(:daisy_ui_tailwind_utils) { ClassGroupUtils.new(TailwindConfig::DEFAULTS) }
|
|
39
43
|
end
|
|
40
44
|
|
|
41
|
-
# Drops the
|
|
45
|
+
# Drops the current Ractor's merger so its next merge picks up
|
|
46
|
+
# configuration changes.
|
|
42
47
|
def reset!
|
|
43
|
-
|
|
48
|
+
Store.delete(:daisy_ui_class_merger)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def build_merger(configuration)
|
|
54
|
+
[configuration, Merger.new(config: configuration.class_merge.merger_config(configuration))]
|
|
44
55
|
end
|
|
45
56
|
end
|
|
46
57
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Configurable
|
|
5
|
+
# @api private The unknown modifiers already reported, so :warn and a
|
|
6
|
+
# callable fire once per key. Remembers up to `limit` keys, then starts
|
|
7
|
+
# over, so modifiers built from user input cannot grow it without bound.
|
|
8
|
+
# One per Ractor (DaisyUI::Store), so its Mutex is never shared.
|
|
9
|
+
class ReportLog
|
|
10
|
+
def initialize(limit)
|
|
11
|
+
@limit = limit
|
|
12
|
+
@keys = Set.new
|
|
13
|
+
@lock = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# True the first time a key is seen.
|
|
17
|
+
def first?(key)
|
|
18
|
+
@lock.synchronize do
|
|
19
|
+
return false if @keys.include?(key)
|
|
20
|
+
|
|
21
|
+
@keys.clear if @keys.size >= @limit
|
|
22
|
+
@keys.add(key)
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear
|
|
28
|
+
@lock.synchronize { @keys.clear }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def size
|
|
32
|
+
@lock.synchronize { @keys.size }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -2,15 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
module DaisyUI
|
|
4
4
|
module Configurable
|
|
5
|
+
# Created when DaisyUI extends this module, at load, so `configuration` is
|
|
6
|
+
# a plain read with no check-then-set race.
|
|
7
|
+
def self.extended(base)
|
|
8
|
+
base.instance_variable_set(:@configuration, Configuration.new)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Yields a writable copy of the configuration, then finalizes the copy
|
|
12
|
+
# (see finalize!) and publishes it. Calling configure again keeps
|
|
13
|
+
# working, no reader (another thread, a non-main Ractor) meets a
|
|
14
|
+
# half-changed or writable configuration, and a block that raises
|
|
15
|
+
# publishes nothing. Calls are serialized, so each starts from what the
|
|
16
|
+
# previous one published.
|
|
5
17
|
def configure
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
configure_lock.synchronize do
|
|
19
|
+
draft = configuration.dup
|
|
20
|
+
yield(draft) if block_given?
|
|
21
|
+
@configuration = draft.finalize!
|
|
22
|
+
ClassMerge.reset!
|
|
23
|
+
finalize!
|
|
24
|
+
configuration
|
|
25
|
+
end
|
|
10
26
|
end
|
|
11
27
|
|
|
12
28
|
def configuration
|
|
13
|
-
@configuration
|
|
29
|
+
@configuration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Freezes what a non-main Ractor reads (this configuration and every
|
|
33
|
+
# component's registries) so DaisyUI::ClassMerge.merge runs in any Ractor.
|
|
34
|
+
# configure calls it, and the Rails engine calls it once the app has
|
|
35
|
+
# booted; an app with neither calls it at the end of boot. Afterwards
|
|
36
|
+
# change settings only through configure: writing to the frozen
|
|
37
|
+
# configuration raises. A configured value that cannot be made shareable
|
|
38
|
+
# is kept as it is (see Shareable.share), never raised on.
|
|
39
|
+
def finalize!
|
|
40
|
+
ClassMerge::DaisyGroups.components.each(&:finalize!)
|
|
41
|
+
configuration.finalize!
|
|
42
|
+
ClassMerge.tailwind_utils
|
|
43
|
+
self
|
|
14
44
|
end
|
|
15
45
|
|
|
16
46
|
class Modifiers
|
|
@@ -19,6 +49,19 @@ module DaisyUI
|
|
|
19
49
|
@groups = {}
|
|
20
50
|
end
|
|
21
51
|
|
|
52
|
+
def initialize_copy(source)
|
|
53
|
+
super
|
|
54
|
+
@modifiers = @modifiers.transform_values(&:dup)
|
|
55
|
+
@groups = @groups.dup
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @api private Called by DaisyUI.finalize!.
|
|
59
|
+
def finalize!
|
|
60
|
+
Shareable.share(@modifiers)
|
|
61
|
+
Shareable.share(@groups)
|
|
62
|
+
freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
22
65
|
# `group:` makes the modifier an alternative to the component's other
|
|
23
66
|
# modifiers in that group, e.g. `add(:huge, classes: "btn-xl",
|
|
24
67
|
# component: DaisyUI::Button, group: :size)`.
|
|
@@ -59,8 +102,24 @@ module DaisyUI
|
|
|
59
102
|
|
|
60
103
|
def initialize
|
|
61
104
|
@prefix = nil
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def initialize_copy(source)
|
|
108
|
+
super
|
|
109
|
+
@class_merge = @class_merge&.dup
|
|
110
|
+
@modifiers = @modifiers&.dup
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @api private Called by DaisyUI.configure and DaisyUI.finalize!. A
|
|
114
|
+
# callable unknown_modifiers that cannot be made shareable (a lambda
|
|
115
|
+
# that closes over app state, an object holding a logger) is kept: the
|
|
116
|
+
# configuration is then frozen but readable on the main Ractor only.
|
|
117
|
+
def finalize!
|
|
118
|
+
class_merge.finalize!
|
|
119
|
+
modifiers.finalize!
|
|
120
|
+
Shareable.share(@prefix)
|
|
121
|
+
Shareable.share(@unknown_modifiers)
|
|
122
|
+
freeze
|
|
64
123
|
end
|
|
65
124
|
|
|
66
125
|
# What a component does with a positional modifier it does not know:
|
|
@@ -94,7 +153,7 @@ module DaisyUI
|
|
|
94
153
|
|
|
95
154
|
# @api private
|
|
96
155
|
def reset_reported_unknown_modifiers!
|
|
97
|
-
|
|
156
|
+
reported_unknown_modifiers.clear
|
|
98
157
|
end
|
|
99
158
|
|
|
100
159
|
def prefix=(prefix)
|
|
@@ -112,16 +171,14 @@ module DaisyUI
|
|
|
112
171
|
|
|
113
172
|
private
|
|
114
173
|
|
|
115
|
-
# Remembers up to REPORTED_UNKNOWN_MODIFIERS_LIMIT keys, then starts over,
|
|
116
|
-
# so modifiers built from user input cannot grow it without bound.
|
|
117
174
|
def first_report?(key)
|
|
118
|
-
|
|
119
|
-
|
|
175
|
+
reported_unknown_modifiers.first?(key)
|
|
176
|
+
end
|
|
120
177
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
178
|
+
# What was already reported lives per Ractor, outside the configuration,
|
|
179
|
+
# which stays shareable.
|
|
180
|
+
def reported_unknown_modifiers
|
|
181
|
+
Store.fetch(:daisy_ui_reported_unknown_modifiers) { ReportLog.new(REPORTED_UNKNOWN_MODIFIERS_LIMIT) }
|
|
125
182
|
end
|
|
126
183
|
|
|
127
184
|
def default_unknown_modifiers
|
|
@@ -130,5 +187,13 @@ module DaisyUI
|
|
|
130
187
|
%w[development test].include?(::Rails.env.to_s) ? :raise : :warn
|
|
131
188
|
end
|
|
132
189
|
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
# Reentrant, and one per Ractor (DaisyUI::Store) so it is never shared;
|
|
194
|
+
# configure writes DaisyUI's ivar, so only the main Ractor gets that far.
|
|
195
|
+
def configure_lock
|
|
196
|
+
Store.fetch(:daisy_ui_configure_lock) { Monitor.new }
|
|
197
|
+
end
|
|
133
198
|
end
|
|
134
199
|
end
|
data/lib/daisy_ui/engine.rb
CHANGED
|
@@ -34,6 +34,13 @@ module DaisyUI
|
|
|
34
34
|
precompile.concat(MINIFIED_ASSETS) if precompile.is_a?(Array)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Once the app has booted, freeze the configuration and component
|
|
38
|
+
# registries so a non-main Ractor can merge classes. DaisyUI.configure in
|
|
39
|
+
# an initializer already finalized; this also covers an app that never
|
|
40
|
+
# calls it and the components loaded during boot (eager loading).
|
|
41
|
+
# DaisyUI.configure keeps working afterwards, on a copy.
|
|
42
|
+
config.after_initialize { DaisyUI.finalize! }
|
|
43
|
+
|
|
37
44
|
initializer "daisy_ui.importmap", before: "importmap" do |app|
|
|
38
45
|
next unless app.config.respond_to?(:importmap)
|
|
39
46
|
|
data/lib/daisy_ui/mcp_server.rb
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# @api private Deep-freezes a value so a non-main Ractor can read it from a
|
|
5
|
+
# constant or a class ivar: `Ractor.make_shareable`, or, on engines without
|
|
6
|
+
# Ractor (JRuby, TruffleRuby), a walk freezing Hash values and Array elements.
|
|
7
|
+
module Shareable
|
|
8
|
+
class << self
|
|
9
|
+
# For the gem's own literals, which must all be shareable: raises if one
|
|
10
|
+
# is not.
|
|
11
|
+
def make(value)
|
|
12
|
+
ractor? ? ::Ractor.make_shareable(value) : deep_freeze(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# For what an app configures. Freezes Hashes, Arrays and Strings all the
|
|
16
|
+
# way down, makes a capture-free Proc shareable, and keeps anything else
|
|
17
|
+
# (a Proc that closes over app state, a logger) as it is, so whatever
|
|
18
|
+
# holds it is readable on the main Ractor only. Never raises, and never
|
|
19
|
+
# hands an object it does not know to Ractor.make_shareable, which
|
|
20
|
+
# freezes as it goes and would leave a logger frozen before failing.
|
|
21
|
+
def share(value)
|
|
22
|
+
case value
|
|
23
|
+
when Hash
|
|
24
|
+
value.each do |key, member|
|
|
25
|
+
share(key)
|
|
26
|
+
share(member)
|
|
27
|
+
end.freeze
|
|
28
|
+
when Array then value.each { |member| share(member) }.freeze
|
|
29
|
+
when String then value.freeze
|
|
30
|
+
when Proc then share_proc(value)
|
|
31
|
+
else value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ractor?
|
|
36
|
+
defined?(::Ractor) ? ::Ractor.respond_to?(:make_shareable) : false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def share_proc(callable)
|
|
42
|
+
ractor? ? ::Ractor.make_shareable(callable) : callable
|
|
43
|
+
rescue ::Ractor::Error, ArgumentError
|
|
44
|
+
callable
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deep_freeze(value)
|
|
48
|
+
case value
|
|
49
|
+
when Hash then value.each_value { |member| deep_freeze(member) }
|
|
50
|
+
when Array then value.each { |member| deep_freeze(member) }
|
|
51
|
+
end
|
|
52
|
+
value.freeze
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module DaisyUI
|
|
6
|
+
# @api private Per-Ractor lazy storage for caches that must not live in a
|
|
7
|
+
# class ivar or a constant (the class-merge merger, the Tailwind class
|
|
8
|
+
# groups, the unknown-modifier report log):
|
|
9
|
+
#
|
|
10
|
+
# DaisyUI::Store.fetch(:daisy_ui_merger) { Merger.new(...) }
|
|
11
|
+
# DaisyUI::Store.delete(:daisy_ui_merger)
|
|
12
|
+
#
|
|
13
|
+
# Adapted from conformance-kit's ConformanceKit::Store (MIT, same author),
|
|
14
|
+
# which daisyui cannot depend on at runtime, plus `delete`. Every Ractor has
|
|
15
|
+
# its own Monitor-guarded table: reentrant, so an initializer may fetch or
|
|
16
|
+
# delete other keys. On Ruby 3.4+ the table comes from
|
|
17
|
+
# `Ractor.store_if_absent`. Below 3.4 the main Ractor's table is created at
|
|
18
|
+
# load (never lazily, so there is no check-then-set race), and a non-main
|
|
19
|
+
# Ractor keeps its own in Ractor-local storage.
|
|
20
|
+
module Store
|
|
21
|
+
# One Ractor's values. Reads skip the lock once a key is stored.
|
|
22
|
+
class Table
|
|
23
|
+
def initialize
|
|
24
|
+
@monitor = Monitor.new
|
|
25
|
+
@values = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fetch(key)
|
|
29
|
+
@values.fetch(key) do
|
|
30
|
+
@monitor.synchronize { @values.fetch(key) { @values[key] = yield } }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete(key)
|
|
35
|
+
@monitor.synchronize { @values.delete(key) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
def fetch(key, &init)
|
|
41
|
+
raise ArgumentError, "DaisyUI::Store.fetch(#{key.inspect}) needs an initializer block" unless init
|
|
42
|
+
|
|
43
|
+
table.fetch(key, &init)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def delete(key)
|
|
47
|
+
table.delete(key)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def main_ractor?
|
|
51
|
+
!defined?(::Ractor) || ::Ractor.current == ::Ractor.main
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def native?
|
|
55
|
+
defined?(::Ractor) ? ::Ractor.respond_to?(:store_if_absent) : false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def table
|
|
61
|
+
return ::Ractor.store_if_absent(:daisy_ui_store) { Table.new } if native?
|
|
62
|
+
return ::Ractor.current[:daisy_ui_store] ||= Table.new unless main_ractor?
|
|
63
|
+
|
|
64
|
+
# Set once below, at load, below Ruby 3.4; only the main Ractor reads it.
|
|
65
|
+
@main_table # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@main_table = Table.new unless native?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# Which words can go into Tailwind's `@source inline("…")`. inline() is a
|
|
6
|
+
# quoted string with brace expansion, and one unbalanced bracket in it
|
|
7
|
+
# makes Tailwind drop every class after it, so a candidate starts and ends
|
|
8
|
+
# like a utility or variant, has balanced () and [], and holds no quote,
|
|
9
|
+
# brace, semicolon, backslash or backtick. A `/` only introduces a
|
|
10
|
+
# fraction or an opacity (`w-1/2`, `bg-base-100/50`), never a path.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module Candidate
|
|
14
|
+
SHAPE = /\A[a-z@!\[*-][^\s"'`;{}\\]*[a-z0-9\])%!]\z/
|
|
15
|
+
PATH = %r{/(?![0-9\[])}
|
|
16
|
+
CLOSING = { ")" => "(", "]" => "[" }.freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def valid?(token)
|
|
20
|
+
SHAPE.match?(token) && token.match?(/[a-z]/) && !PATH.match?(token) && balanced?(token)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The candidates among the whitespace-separated words of `classes`.
|
|
24
|
+
def tokens(classes)
|
|
25
|
+
classes.split.select { |token| valid?(token) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def balanced?(token)
|
|
31
|
+
open = []
|
|
32
|
+
token.each_char do |char|
|
|
33
|
+
if CLOSING.value?(char)
|
|
34
|
+
open << char
|
|
35
|
+
elsif CLOSING.key?(char)
|
|
36
|
+
return false unless open.pop == CLOSING[char]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
open.empty?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|