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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# `daisyui-tailwind`: the Tailwind sources for the components an app uses,
|
|
6
|
+
# for its CSS build to call (`bundle exec daisyui-tailwind app -o …`).
|
|
7
|
+
# Returns the exit status; usage errors print to `err` and return 1.
|
|
8
|
+
class Command
|
|
9
|
+
BANNER = <<~TEXT
|
|
10
|
+
Usage: daisyui-tailwind [options] [PATH...]
|
|
11
|
+
|
|
12
|
+
Prints Tailwind CSS v4 sources (@source inline) for the DaisyUI components
|
|
13
|
+
that the Ruby and templates under each PATH use, plus the components those
|
|
14
|
+
render. @import the output from your Tailwind entry.
|
|
15
|
+
|
|
16
|
+
TEXT
|
|
17
|
+
|
|
18
|
+
def self.run(argv, out: $stdout, err: $stderr) = new(out:, err:).run(argv)
|
|
19
|
+
|
|
20
|
+
def initialize(out:, err:)
|
|
21
|
+
@out = out
|
|
22
|
+
@err = err
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(argv)
|
|
26
|
+
require "optparse"
|
|
27
|
+
|
|
28
|
+
options = { components: [], format: "css", responsive: [] }
|
|
29
|
+
parser = parser(options)
|
|
30
|
+
paths = parser.parse(argv)
|
|
31
|
+
return help(parser) if options[:help]
|
|
32
|
+
return usage(parser) if paths.empty? && options[:components].empty?
|
|
33
|
+
|
|
34
|
+
result = Tailwind.sources(scan: paths, **options.slice(:components, :format, :responsive, :prefix))
|
|
35
|
+
options[:output] ? write(options[:output], result) : @out.write(result)
|
|
36
|
+
0
|
|
37
|
+
rescue OptionParser::ParseError, ArgumentError, SystemCallError => e
|
|
38
|
+
@err.puts "daisyui-tailwind: #{e.message}"
|
|
39
|
+
1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parser(options)
|
|
45
|
+
OptionParser.new do |opts|
|
|
46
|
+
opts.banner = BANNER
|
|
47
|
+
opts.on("-c", "--component NAMES", Array, "Components to include, e.g. Button,Navbar (repeatable)") do |names|
|
|
48
|
+
options[:components].concat(names)
|
|
49
|
+
end
|
|
50
|
+
opts.on("-f", "--format FORMAT", %w[css json], "css (default) or json") { |format| options[:format] = format }
|
|
51
|
+
opts.on("-o", "--output FILE", "Write to FILE, only when its content changes") { |file| options[:output] = file }
|
|
52
|
+
opts.on("--responsive BREAKPOINTS", Array, "Add breakpoint variants, e.g. sm,md,lg") do |breakpoints|
|
|
53
|
+
options[:responsive].concat(breakpoints)
|
|
54
|
+
end
|
|
55
|
+
opts.on("--prefix PREFIX", "The DaisyUI.configuration.prefix your app sets") { |prefix| options[:prefix] = prefix }
|
|
56
|
+
opts.on("-h", "--help", "Show this help") { options[:help] = true }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def help(parser)
|
|
61
|
+
@out.puts parser
|
|
62
|
+
0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def usage(parser)
|
|
66
|
+
@err.puts parser
|
|
67
|
+
1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Only on change: a CSS watcher that sees its own input rewritten on
|
|
71
|
+
# every build rebuilds forever.
|
|
72
|
+
def write(path, content)
|
|
73
|
+
if File.exist?(path) && File.read(path) == content
|
|
74
|
+
@err.puts "daisyui-tailwind: #{path} is up to date"
|
|
75
|
+
else
|
|
76
|
+
require "fileutils"
|
|
77
|
+
|
|
78
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
79
|
+
File.write(path, content)
|
|
80
|
+
@err.puts "daisyui-tailwind: wrote #{path}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# Reads a component's Ruby the way the manifest needs it: the words of its
|
|
6
|
+
# string literals (where its sub-part classes live, e.g. "card-body") and
|
|
7
|
+
# the constants its code references (the components it renders). Ripper
|
|
8
|
+
# keeps comments out of both.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Lexer
|
|
12
|
+
class << self
|
|
13
|
+
def strings(source)
|
|
14
|
+
tokens(source, :on_tstring_content).flat_map(&:split)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def constants(source)
|
|
18
|
+
tokens(source, :on_const).uniq
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def tokens(source, type)
|
|
24
|
+
require "ripper"
|
|
25
|
+
|
|
26
|
+
::Ripper.lex(source).filter_map { |(_position, kind, token)| token if kind == type }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# The committed tailwind_manifest.json: per component (by short name, "Button"),
|
|
6
|
+
# its class, daisyUI name, files, the components it renders and its
|
|
7
|
+
# classes; `shared` is base.rb, which every component draws on. The JSON
|
|
8
|
+
# is the contract for build scripts in other languages, so its keys are
|
|
9
|
+
# documented in the README.
|
|
10
|
+
class Manifest
|
|
11
|
+
def self.load(path = MANIFEST_PATH, root: ROOT)
|
|
12
|
+
require "json"
|
|
13
|
+
|
|
14
|
+
new(JSON.parse(File.read(path)), root:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :components, :shared, :root
|
|
18
|
+
|
|
19
|
+
def initialize(data, root: ROOT)
|
|
20
|
+
@components = data.fetch("components")
|
|
21
|
+
@shared = data.fetch("shared")
|
|
22
|
+
@root = root
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def component_names = components.keys
|
|
26
|
+
|
|
27
|
+
# `names` ("Button" or "DaisyUI::Button") plus every component they
|
|
28
|
+
# render, transitively. Sorted.
|
|
29
|
+
def resolve(names)
|
|
30
|
+
resolved = []
|
|
31
|
+
pending = names.map { |name| canonical(name) }
|
|
32
|
+
until pending.empty?
|
|
33
|
+
name = pending.pop
|
|
34
|
+
next if resolved.include?(name)
|
|
35
|
+
|
|
36
|
+
resolved << name
|
|
37
|
+
pending.concat(components.fetch(name)["depends_on"])
|
|
38
|
+
end
|
|
39
|
+
resolved.sort
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Absolute paths of the components' files, for a literal `@source`.
|
|
43
|
+
def files(names) = names.flat_map { |name| absolute(components.fetch(name)["files"]) }
|
|
44
|
+
|
|
45
|
+
def shared_files = absolute(shared["files"])
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def canonical(name)
|
|
50
|
+
short = name.to_s.delete_prefix("DaisyUI::")
|
|
51
|
+
return short if components.key?(short)
|
|
52
|
+
|
|
53
|
+
raise UnknownComponentError.new(name, component_names)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def absolute(paths) = paths.map { |path| File.join(root, path) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# Builds data/daisyui/tailwind_manifest.json from the components
|
|
6
|
+
# themselves (`bundle exec rake tailwind:manifest`); the manifest spec
|
|
7
|
+
# fails while the committed file differs. Per component:
|
|
8
|
+
#
|
|
9
|
+
# - `classes`: its component class (a Symbol Tailwind cannot see), its
|
|
10
|
+
# registered modifiers and the class words in its Ruby's strings
|
|
11
|
+
# - `modifiers`: the registered modifier classes, which `responsive:`
|
|
12
|
+
# can prefix with a breakpoint
|
|
13
|
+
# - `depends_on`: the other components its code references
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module ManifestBuilder
|
|
17
|
+
SCHEMA = 1
|
|
18
|
+
SHARED_FILE = "lib/daisy_ui/base.rb"
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def build(root: ROOT)
|
|
22
|
+
components = gem_components(File.join(root, "lib", "daisy_ui"))
|
|
23
|
+
names = components.map(&:first)
|
|
24
|
+
{
|
|
25
|
+
"schema" => SCHEMA,
|
|
26
|
+
"shared" => shared(root),
|
|
27
|
+
"components" => components.to_h { |name, klass, file| [name, entry(klass, file:, names:, root:)] }
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def generate(root: ROOT)
|
|
32
|
+
require "json"
|
|
33
|
+
|
|
34
|
+
"#{JSON.pretty_generate(build(root:))}\n"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def write(path = MANIFEST_PATH, root: ROOT)
|
|
38
|
+
json = generate(root:)
|
|
39
|
+
File.write(path, json) unless File.exist?(path) && File.read(path) == json
|
|
40
|
+
path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def entry(klass, file:, names:, root:)
|
|
44
|
+
source = File.read(file)
|
|
45
|
+
name = klass.component_class&.to_s
|
|
46
|
+
modifiers = modifier_classes(klass)
|
|
47
|
+
{
|
|
48
|
+
"class" => klass.name,
|
|
49
|
+
"name" => name,
|
|
50
|
+
"files" => [file.delete_prefix("#{root}/")],
|
|
51
|
+
"depends_on" => ((Lexer.constants(source) & names) - [short_name(klass)]).sort,
|
|
52
|
+
"modifiers" => modifiers,
|
|
53
|
+
"classes" => ([name, *modifiers].compact | Candidate.tokens(Lexer.strings(source).join(" "))).sort
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# The gem's own components, by short name: a DaisyUI:: class defined
|
|
60
|
+
# in a file under lib/daisy_ui (not one a spec or an app defined).
|
|
61
|
+
def gem_components(lib)
|
|
62
|
+
ClassMerge::DaisyGroups.gem_components.filter_map do |klass|
|
|
63
|
+
file = Object.const_source_location(klass.name)&.first
|
|
64
|
+
[short_name(klass), klass, file] if file&.start_with?("#{lib}/")
|
|
65
|
+
end.sort_by(&:first)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def shared(root)
|
|
69
|
+
source = File.read(File.join(root, SHARED_FILE))
|
|
70
|
+
{ "files" => [SHARED_FILE], "classes" => Candidate.tokens(Lexer.strings(source).join(" ")).uniq.sort }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def modifier_classes(klass)
|
|
74
|
+
classes = (klass.modifiers || {}).values.flat_map { |value| value.to_s.split }.uniq.sort
|
|
75
|
+
invalid = classes.reject { |token| Candidate.valid?(token) }
|
|
76
|
+
return classes if invalid.empty?
|
|
77
|
+
|
|
78
|
+
raise ArgumentError, "#{klass.name} registers #{invalid.map(&:inspect).join(', ')}, " \
|
|
79
|
+
"which Tailwind cannot read from @source inline(…)"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def short_name(klass) = klass.name.split("::").last
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# Renders the Tailwind sources for a set of (resolved) components:
|
|
6
|
+
#
|
|
7
|
+
# - `to_css`: a partial for the app's Tailwind entry to @import, one
|
|
8
|
+
# `@source inline("…")` per component after base.rb's shared classes. It
|
|
9
|
+
# names no path, so it is the same on every machine.
|
|
10
|
+
# - `to_json`: the same for a build script, plus each component's absolute
|
|
11
|
+
# files for anyone who wants a literal `@source` instead.
|
|
12
|
+
#
|
|
13
|
+
# `responsive:` adds `<breakpoint>:` variants of the classes `responsive:`
|
|
14
|
+
# renders (a component's own class and its modifiers). `prefix:` mirrors
|
|
15
|
+
# DaisyUI.configuration.prefix: the component class and modifiers always
|
|
16
|
+
# render prefixed, while a class string in a component's Ruby may render
|
|
17
|
+
# either way (`component_classes("card-body")` is prefixed, Tooltip's
|
|
18
|
+
# "after:hidden" is not), so those are given both ways.
|
|
19
|
+
class Sources
|
|
20
|
+
BREAKPOINT = /\A@?[a-z0-9][a-z0-9-]*\z/
|
|
21
|
+
PREFIX = /\A[^\s"'`;{}\\]+\z/
|
|
22
|
+
|
|
23
|
+
attr_reader :components
|
|
24
|
+
|
|
25
|
+
def initialize(manifest, components, responsive: [], prefix: nil)
|
|
26
|
+
@manifest = manifest
|
|
27
|
+
@components = components
|
|
28
|
+
@breakpoints = responsive.map(&:to_s)
|
|
29
|
+
@prefix = prefix.to_s
|
|
30
|
+
validate!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render(format)
|
|
34
|
+
case format.to_s
|
|
35
|
+
when "css" then to_css
|
|
36
|
+
when "json" then to_json
|
|
37
|
+
else raise ArgumentError, "unknown format #{format.inspect}: css or json"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_css
|
|
42
|
+
header = "/* Generated by daisyui-tailwind (daisyui #{VERSION}): do not edit. */\n"
|
|
43
|
+
return "#{header}/* No DaisyUI components referenced. */\n" if components.empty?
|
|
44
|
+
|
|
45
|
+
lines = [
|
|
46
|
+
"/* daisyUI components: #{components.join(', ')} */",
|
|
47
|
+
"/* Shared by every component (lib/daisy_ui/base.rb) */",
|
|
48
|
+
inline(shared_classes)
|
|
49
|
+
]
|
|
50
|
+
components.each { |name| lines.push("/* #{@manifest.components.fetch(name)['class']} */", inline(classes(name))) }
|
|
51
|
+
"#{header}#{lines.join("\n")}\n"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_json(*)
|
|
55
|
+
require "json"
|
|
56
|
+
|
|
57
|
+
"#{JSON.pretty_generate(json)}\n"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def json
|
|
63
|
+
files = @manifest.shared_files + @manifest.files(components)
|
|
64
|
+
{
|
|
65
|
+
"daisyui" => VERSION,
|
|
66
|
+
"components" => components.to_h { |name| [name, component_json(name)] },
|
|
67
|
+
"shared" => { "files" => @manifest.shared_files, "classes" => shared_classes },
|
|
68
|
+
"classes" => (shared_classes + components.flat_map { |name| classes(name) }).uniq.sort,
|
|
69
|
+
"files" => files.uniq.sort
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def component_json(name)
|
|
74
|
+
entry = @manifest.components.fetch(name)
|
|
75
|
+
entry.slice("class", "name", "depends_on").merge("files" => @manifest.files([name]), "classes" => classes(name))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def classes(name)
|
|
79
|
+
entry = @manifest.components.fetch(name)
|
|
80
|
+
expand(entry["classes"], [entry["name"], *entry["modifiers"]].compact)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def shared_classes = expand(@manifest.shared["classes"], @manifest.shared["classes"])
|
|
84
|
+
|
|
85
|
+
def expand(classes, responsive)
|
|
86
|
+
prefixed = classes.map { |token| "#{@prefix}#{token}" }
|
|
87
|
+
prefixed += classes - responsive unless @prefix.empty?
|
|
88
|
+
prefixed + @breakpoints.flat_map { |breakpoint| responsive.map { |token| "#{breakpoint}:#{@prefix}#{token}" } }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def inline(classes) = %(@source inline("#{classes.join(' ')}");)
|
|
92
|
+
|
|
93
|
+
def validate!
|
|
94
|
+
@breakpoints.each do |breakpoint|
|
|
95
|
+
raise ArgumentError, "invalid breakpoint #{breakpoint.inspect}: give the variant name, e.g. sm or @md" unless BREAKPOINT.match?(breakpoint)
|
|
96
|
+
end
|
|
97
|
+
return if @prefix.empty? || PREFIX.match?(@prefix)
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, "invalid prefix #{@prefix.inspect}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "did_you_mean"
|
|
4
|
+
|
|
5
|
+
module DaisyUI
|
|
6
|
+
module Tailwind
|
|
7
|
+
# A component name (`--component Buton`) the manifest does not have.
|
|
8
|
+
class UnknownComponentError < ArgumentError
|
|
9
|
+
def initialize(name, known)
|
|
10
|
+
suggestions = DidYouMean::SpellChecker.new(dictionary: known).correct(name.to_s)
|
|
11
|
+
message = "DaisyUI has no component #{name}"
|
|
12
|
+
message = "#{message}. Did you mean? #{suggestions.join(', ')}" unless suggestions.empty?
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
module Tailwind
|
|
5
|
+
# Finds the components an app's Ruby and templates reference:
|
|
6
|
+
#
|
|
7
|
+
# - the constant: `DaisyUI::Card`, `class AppNavbar < DaisyUI::Navbar`
|
|
8
|
+
# - an instance: `Card.new(…)`
|
|
9
|
+
# - a kit call, with parentheses (`Button(:primary)`) or without
|
|
10
|
+
# (`Badge :primary do`, `Hero do`, `Modal { … }`, `Toast class: "…"`,
|
|
11
|
+
# `Button variant do`)
|
|
12
|
+
#
|
|
13
|
+
# Comments are skipped (each file's own kind: `#` lines in Ruby, `<%# %>`
|
|
14
|
+
# in ERB, `-#` in HAML, `/` in Slim), so "# Loading state…" is not a use
|
|
15
|
+
# of Loading, while a HAML `#sidebar= …` line still counts. A name is matched whole: `MyButton(` and `UI::Card(` are not
|
|
16
|
+
# uses (an app wrapper `UI::Card < DaisyUI::Card` is found by its own
|
|
17
|
+
# file). Matching on text over-reports rather than misses: a false hit
|
|
18
|
+
# costs some CSS, a miss leaves a component unstyled.
|
|
19
|
+
class Usage
|
|
20
|
+
COMMENTS = {
|
|
21
|
+
"rb" => /^[ \t]*#.*$/,
|
|
22
|
+
"erb" => /<%#.*?%>/m,
|
|
23
|
+
"haml" => /^[ \t]*-#.*$/,
|
|
24
|
+
"slim" => %r{^[ \t]*/.*$}
|
|
25
|
+
}.freeze
|
|
26
|
+
EXTENSIONS = COMMENTS.keys.freeze
|
|
27
|
+
# What follows a name in a call: `(`, `.new`, a block, or arguments
|
|
28
|
+
# without parentheses: `:primary`, `class: …`, `"…"`, a splat
|
|
29
|
+
# (`*modifiers`, `**options`, `&block`), or a variable that ends the line
|
|
30
|
+
# or is followed by `,`, `do` or `{` (`Button variant do`), which keeps
|
|
31
|
+
# "Join thousands of…" in a string from counting.
|
|
32
|
+
CALL = /
|
|
33
|
+
\( | \.new\b | [ \t]*\{ |
|
|
34
|
+
[ \t]+ (?: do\b | :[a-z_] | ["'] | [a-z_]\w*:[ \t] | (?: \*\*? | & ) [a-z_@(] |
|
|
35
|
+
[a-z_@][\w@]* (?: \.[a-z_]\w* )* [ \t]* (?: , | do\b | \{ | $ ) )
|
|
36
|
+
/x
|
|
37
|
+
|
|
38
|
+
def initialize(names)
|
|
39
|
+
names = names.sort_by { |name| -name.length }.map { |name| Regexp.escape(name) }.join("|")
|
|
40
|
+
@pattern = /DaisyUI::(#{names})\b|(?<![A-Za-z0-9_:])(#{names})(?=#{CALL})/
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Sorted names referenced in the files under `paths` (directories or
|
|
44
|
+
# files). A path that does not exist raises: a typo must not quietly
|
|
45
|
+
# leave every component out.
|
|
46
|
+
def scan(paths)
|
|
47
|
+
files(paths).flat_map { |file| references(File.read(file).scrub, kind: File.extname(file).delete_prefix(".")) }
|
|
48
|
+
.uniq.sort
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sorted names referenced in `source`, a file of `kind` (its extension).
|
|
52
|
+
def references(source, kind: "rb")
|
|
53
|
+
source.gsub(COMMENTS.fetch(kind, COMMENTS["rb"]), "").scan(@pattern).flat_map(&:compact).uniq.sort
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def files(paths)
|
|
59
|
+
paths.flat_map do |path|
|
|
60
|
+
path = path.to_s
|
|
61
|
+
if File.directory?(path)
|
|
62
|
+
Dir.glob(File.join(path, "**", "*.{#{EXTENSIONS.join(',')}}"))
|
|
63
|
+
elsif File.file?(path)
|
|
64
|
+
[path]
|
|
65
|
+
else
|
|
66
|
+
raise ArgumentError, "no such file or directory: #{path}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DaisyUI
|
|
4
|
+
# Tailwind CSS sources for only the daisyUI components an app renders.
|
|
5
|
+
#
|
|
6
|
+
# Pointing Tailwind's `@source` at the whole gem hands it every class of
|
|
7
|
+
# every component (and ClassMerge's catalogue of Tailwind utilities), yet
|
|
8
|
+
# still misses what Tailwind v4 cannot see in Ruby: a Symbol component class
|
|
9
|
+
# (`self.component_class = :btn`) and anything in a comment. The gem's
|
|
10
|
+
# manifest (data/daisyui/tailwind_manifest.json, built from the components by
|
|
11
|
+
# `rake tailwind:manifest`) lists each component's classes and the
|
|
12
|
+
# components it renders, so an app's CSS build gets `@source inline(…)` for
|
|
13
|
+
# just the components it uses:
|
|
14
|
+
#
|
|
15
|
+
# DaisyUI::Tailwind.sources(scan: ["app/views", "app/components"])
|
|
16
|
+
# DaisyUI::Tailwind.sources(components: %w[Button Navbar], format: :json)
|
|
17
|
+
#
|
|
18
|
+
# `bundle exec daisyui-tailwind` (DaisyUI::Tailwind::Command) does the same
|
|
19
|
+
# from a build script.
|
|
20
|
+
module Tailwind
|
|
21
|
+
ROOT = File.expand_path("../..", __dir__).freeze
|
|
22
|
+
# In the gem's data/ dir, not lib/: an app that still points Tailwind at
|
|
23
|
+
# the gem's lib/ must not start scanning it.
|
|
24
|
+
MANIFEST_PATH = File.join(ROOT, "data", "daisyui", "tailwind_manifest.json").freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def manifest = Manifest.load
|
|
28
|
+
|
|
29
|
+
# The components the Ruby and templates under `scan` reference, plus the
|
|
30
|
+
# `components` named, plus every component those render. Sorted names.
|
|
31
|
+
def used_components(scan: [], components: [], manifest: self.manifest)
|
|
32
|
+
found = scan.empty? ? [] : Usage.new(manifest.component_names).scan(scan)
|
|
33
|
+
manifest.resolve(found + components)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# CSS (`format: :css`, for Tailwind to @import) or JSON for those
|
|
37
|
+
# components. `responsive:` adds breakpoint variants (`%w[sm md]`) of the
|
|
38
|
+
# classes `responsive:` renders; `prefix:` defaults to the
|
|
39
|
+
# DaisyUI.configuration.prefix the components render with.
|
|
40
|
+
def sources(scan: [], components: [], format: :css, responsive: [], prefix: DaisyUI.configuration.prefix)
|
|
41
|
+
manifest = self.manifest
|
|
42
|
+
used = used_components(scan:, components:, manifest:)
|
|
43
|
+
Sources.new(manifest, used, responsive:, prefix:).render(format)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/daisy_ui/updated_at.rb
CHANGED
data/lib/daisy_ui/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: daisyui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -55,6 +55,7 @@ description: DaisyUI is a comprehensive Ruby UI component library that brings Da
|
|
|
55
55
|
email: mikael@zoolutions.llc
|
|
56
56
|
executables:
|
|
57
57
|
- daisyui-mcp
|
|
58
|
+
- daisyui-tailwind
|
|
58
59
|
extensions: []
|
|
59
60
|
extra_rdoc_files: []
|
|
60
61
|
files:
|
|
@@ -69,7 +70,9 @@ files:
|
|
|
69
70
|
- app/javascript/daisy_ui/controllers/daisy_tooltip_controller.min.js
|
|
70
71
|
- app/javascript/daisy_ui/controllers/daisy_tooltip_controller.min.js.map
|
|
71
72
|
- config/importmap.rb
|
|
73
|
+
- data/daisyui/tailwind_manifest.json
|
|
72
74
|
- exe/daisyui-mcp
|
|
75
|
+
- exe/daisyui-tailwind
|
|
73
76
|
- lib/daisy_ui.rb
|
|
74
77
|
- lib/daisy_ui/accordion.rb
|
|
75
78
|
- lib/daisy_ui/alert.rb
|
|
@@ -98,6 +101,7 @@ files:
|
|
|
98
101
|
- lib/daisy_ui/collapse.rb
|
|
99
102
|
- lib/daisy_ui/collapsible_sub_menu.rb
|
|
100
103
|
- lib/daisy_ui/configurable.rb
|
|
104
|
+
- lib/daisy_ui/configurable/report_log.rb
|
|
101
105
|
- lib/daisy_ui/countdown.rb
|
|
102
106
|
- lib/daisy_ui/diff.rb
|
|
103
107
|
- lib/daisy_ui/divider.rb
|
|
@@ -141,11 +145,13 @@ files:
|
|
|
141
145
|
- lib/daisy_ui/range.rb
|
|
142
146
|
- lib/daisy_ui/rating.rb
|
|
143
147
|
- lib/daisy_ui/select.rb
|
|
148
|
+
- lib/daisy_ui/shareable.rb
|
|
144
149
|
- lib/daisy_ui/skeleton.rb
|
|
145
150
|
- lib/daisy_ui/stack.rb
|
|
146
151
|
- lib/daisy_ui/stat.rb
|
|
147
152
|
- lib/daisy_ui/status.rb
|
|
148
153
|
- lib/daisy_ui/steps.rb
|
|
154
|
+
- lib/daisy_ui/store.rb
|
|
149
155
|
- lib/daisy_ui/sub_menu.rb
|
|
150
156
|
- lib/daisy_ui/swap.rb
|
|
151
157
|
- lib/daisy_ui/tab.rb
|
|
@@ -154,6 +160,15 @@ files:
|
|
|
154
160
|
- lib/daisy_ui/table.rb
|
|
155
161
|
- lib/daisy_ui/table_row.rb
|
|
156
162
|
- lib/daisy_ui/tabs.rb
|
|
163
|
+
- lib/daisy_ui/tailwind.rb
|
|
164
|
+
- lib/daisy_ui/tailwind/candidate.rb
|
|
165
|
+
- lib/daisy_ui/tailwind/command.rb
|
|
166
|
+
- lib/daisy_ui/tailwind/lexer.rb
|
|
167
|
+
- lib/daisy_ui/tailwind/manifest.rb
|
|
168
|
+
- lib/daisy_ui/tailwind/manifest_builder.rb
|
|
169
|
+
- lib/daisy_ui/tailwind/sources.rb
|
|
170
|
+
- lib/daisy_ui/tailwind/unknown_component_error.rb
|
|
171
|
+
- lib/daisy_ui/tailwind/usage.rb
|
|
157
172
|
- lib/daisy_ui/text_rotate.rb
|
|
158
173
|
- lib/daisy_ui/textarea.rb
|
|
159
174
|
- lib/daisy_ui/theme_controller.rb
|