syntax_tree-tailwindcss 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4b9561bf73bff9b6e6a4c31fae7d4ff8d287625512a657baf29fd5ffe15a73cd
4
+ data.tar.gz: 3211cff998a98e08555fd66e19570e4f3ec5f0ce3064d33cd4088195adc11d3f
5
+ SHA512:
6
+ metadata.gz: d1153927f9f76770ab2453c88761534d7c653df77afafd460e8db289734f04936c58b1406a0789b429428f3211b1e2bdd95ca89fbf167e3f4e7880b11610501e
7
+ data.tar.gz: 38ec5cf289bd018651f8f5efb64e0b0877edc193b9998d7bfba3d4024ff96b1132859207ead2f193874e13683ba6f5ebbdd921f8f7f0eb37193d07fb6f662915
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ inherit_gem:
2
+ syntax_tree: config/rubocop.yml
3
+
4
+ AllCops:
5
+ DisplayCopNames: true
6
+ DisplayStyleGuide: true
7
+ NewCops: enable
8
+ SuggestExtensions: false
9
+ TargetRubyVersion: 3.0
10
+
11
+ Metrics:
12
+ Enabled: false
13
+
14
+ Style/Documentation:
15
+ Enabled: false
16
+
17
+ Naming/MethodName:
18
+ Enabled: false
19
+
20
+ Naming/MethodParameterName:
21
+ Enabled: false
22
+
23
+ Layout/LineLength:
24
+ Enabled: false
25
+
26
+ Style/GuardClause:
27
+ Enabled: false
28
+
29
+ Style/DocumentDynamicEvalDefinition:
30
+ Exclude:
31
+ - test/fixture/**/*.rb
data/.streerc ADDED
@@ -0,0 +1 @@
1
+ --print-width=100
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Tomasz Szczęśniak-Szlagowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # SyntaxTree plugin: tailwindcss
2
+
3
+ SyntaxTree can be used to format:
4
+
5
+ - `.rb` files that render HTML, e.g. Rails helpers
6
+ - `.haml` files (using the [haml](https://github.com/ruby-syntax-tree/syntax_tree-haml)
7
+ plugin)
8
+ - `.html.erb` files (using the [erb](https://github.com/davidwessman/syntax_tree-erb)
9
+ plugin)
10
+
11
+ All of these can contain TailwindCSS classes and this plugin will help you keep
12
+ them in order, just like
13
+ [the official prettier plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss)
14
+ would.
15
+
16
+ ## Installation
17
+
18
+ Install the gem and add to the application's Gemfile by executing:
19
+
20
+ $ bundle add syntax_tree-tailwindcss
21
+
22
+ If bundler is not being used to manage dependencies, install the gem by executing:
23
+
24
+ $ gem install syntax_tree-tailwindcss
25
+
26
+ ## Usage
27
+
28
+ Example:
29
+
30
+ $ stree write --plugins=erb,tailwindcss app/helpers/**/*.rb app/views/**/*.html.erb
31
+
32
+ ## Contributing
33
+
34
+ Bug reports and pull requests are welcome on GitHub at https://github.com/spect88/syntax_tree-tailwindcss.
35
+
36
+ ## License
37
+
38
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+ require "syntax_tree/rake_tasks"
6
+ require "rubocop/rake_task"
7
+
8
+ Minitest::TestTask.create
9
+
10
+ stree_config =
11
+ proc { |t| t.source_files = FileList[%w[Gemfile Rakefile lib/**/*.rb test/**/*.rb *.gemspec]] }
12
+ SyntaxTree::Rake::CheckTask.new(&stree_config)
13
+ SyntaxTree::Rake::WriteTask.new(&stree_config)
14
+
15
+ RuboCop::RakeTask.new
16
+
17
+ desc "Run all code checks"
18
+ task check: %i[rubocop stree:check test]
19
+
20
+ desc "Run all automated fixes"
21
+ task fix: %i[stree:write rubocop:autocorrect_all]
22
+
23
+ task default: :check
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ # This is a fix for SyntaxTree::MutationVisitor not traversing the entire tree
5
+ class CompleteMutationVisitor < SyntaxTree::BasicVisitor
6
+ def initialize
7
+ super
8
+ @mutations = []
9
+ end
10
+
11
+ def mutate(query, &block)
12
+ @mutations << [Pattern.new(query).compile, block]
13
+ end
14
+
15
+ def visit(node)
16
+ return unless node
17
+
18
+ result = node.accept(self)
19
+ @mutations.each do |(pattern, mutation)|
20
+ result = mutation.call(result) if pattern.call(result)
21
+ end
22
+
23
+ result
24
+ end
25
+
26
+ def copy_and_visit_children(node)
27
+ node.dup.tap do |clone|
28
+ clone.instance_variables.each do |name|
29
+ next if %i[@location @comments].include?(name)
30
+
31
+ var = clone.instance_variable_get(name)
32
+ clone.instance_variable_set(name, visit_child(var))
33
+ end
34
+ end
35
+ end
36
+
37
+ def visit_child(child)
38
+ case child
39
+ when Array
40
+ child.map { |node| visit_child(node) }
41
+ when SyntaxTree::Node
42
+ visit(child)
43
+ else
44
+ child
45
+ end
46
+ end
47
+
48
+ alias visit_aref copy_and_visit_children
49
+ alias visit_aref_field copy_and_visit_children
50
+ alias visit_alias copy_and_visit_children
51
+ alias visit_arg_block copy_and_visit_children
52
+ alias visit_arg_paren copy_and_visit_children
53
+ alias visit_arg_star copy_and_visit_children
54
+ alias visit_args copy_and_visit_children
55
+ alias visit_args_forward copy_and_visit_children
56
+ alias visit_array copy_and_visit_children
57
+ alias visit_aryptn copy_and_visit_children
58
+ alias visit_assign copy_and_visit_children
59
+ alias visit_assoc copy_and_visit_children
60
+ alias visit_assoc_splat copy_and_visit_children
61
+ alias visit_backref copy_and_visit_children
62
+ alias visit_backtick copy_and_visit_children
63
+ alias visit_bare_assoc_hash copy_and_visit_children
64
+ alias visit_BEGIN copy_and_visit_children
65
+ alias visit_begin copy_and_visit_children
66
+ alias visit_binary copy_and_visit_children
67
+ alias visit_block copy_and_visit_children
68
+ alias visit_blockarg copy_and_visit_children
69
+ alias visit_block_var copy_and_visit_children
70
+ alias visit_bodystmt copy_and_visit_children
71
+ alias visit_break copy_and_visit_children
72
+ alias visit_call copy_and_visit_children
73
+ alias visit_case copy_and_visit_children
74
+ alias visit_CHAR copy_and_visit_children
75
+ alias visit_class copy_and_visit_children
76
+ alias visit_comma copy_and_visit_children
77
+ alias visit_command copy_and_visit_children
78
+ alias visit_command_call copy_and_visit_children
79
+ alias visit_comment copy_and_visit_children
80
+ alias visit_const copy_and_visit_children
81
+ alias visit_const_path_field copy_and_visit_children
82
+ alias visit_const_path_ref copy_and_visit_children
83
+ alias visit_const_ref copy_and_visit_children
84
+ alias visit_cvar copy_and_visit_children
85
+ alias visit_def copy_and_visit_children
86
+ alias visit_defined copy_and_visit_children
87
+ alias visit_dyna_symbol copy_and_visit_children
88
+ alias visit_END copy_and_visit_children
89
+ alias visit_else copy_and_visit_children
90
+ alias visit_elsif copy_and_visit_children
91
+ alias visit_embdoc copy_and_visit_children
92
+ alias visit_embexpr_beg copy_and_visit_children
93
+ alias visit_embexpr_end copy_and_visit_children
94
+ alias visit_embvar copy_and_visit_children
95
+ alias visit_ensure copy_and_visit_children
96
+ alias visit_excessed_comma copy_and_visit_children
97
+ alias visit_field copy_and_visit_children
98
+ alias visit_float copy_and_visit_children
99
+ alias visit_fndptn copy_and_visit_children
100
+ alias visit_for copy_and_visit_children
101
+ alias visit_gvar copy_and_visit_children
102
+ alias visit_hash copy_and_visit_children
103
+ alias visit_heredoc copy_and_visit_children
104
+ alias visit_heredoc_beg copy_and_visit_children
105
+ alias visit_heredoc_end copy_and_visit_children
106
+ alias visit_hshptn copy_and_visit_children
107
+ alias visit_ident copy_and_visit_children
108
+ alias visit_if copy_and_visit_children
109
+ alias visit_if_op copy_and_visit_children
110
+ alias visit_imaginary copy_and_visit_children
111
+ alias visit_in copy_and_visit_children
112
+ alias visit_int copy_and_visit_children
113
+ alias visit_ivar copy_and_visit_children
114
+ alias visit_kw copy_and_visit_children
115
+ alias visit_kwrest_param copy_and_visit_children
116
+ alias visit_label copy_and_visit_children
117
+ alias visit_label_end copy_and_visit_children
118
+ alias visit_lambda copy_and_visit_children
119
+ alias visit_lambda_var copy_and_visit_children
120
+ alias visit_lbrace copy_and_visit_children
121
+ alias visit_lbracket copy_and_visit_children
122
+ alias visit_lparen copy_and_visit_children
123
+ alias visit_massign copy_and_visit_children
124
+ alias visit_method_add_block copy_and_visit_children
125
+ alias visit_mlhs copy_and_visit_children
126
+ alias visit_mlhs_paren copy_and_visit_children
127
+ alias visit_module copy_and_visit_children
128
+ alias visit_mrhs copy_and_visit_children
129
+ alias visit_next copy_and_visit_children
130
+ alias visit_not copy_and_visit_children
131
+ alias visit_op copy_and_visit_children
132
+ alias visit_opassign copy_and_visit_children
133
+ alias visit_params copy_and_visit_children
134
+ alias visit_paren copy_and_visit_children
135
+ alias visit_period copy_and_visit_children
136
+ alias visit_pinned_begin copy_and_visit_children
137
+ alias visit_pinned_var_ref copy_and_visit_children
138
+ alias visit_program copy_and_visit_children
139
+ alias visit_qsymbols copy_and_visit_children
140
+ alias visit_qsymbols_beg copy_and_visit_children
141
+ alias visit_qwords copy_and_visit_children
142
+ alias visit_qwords_beg copy_and_visit_children
143
+ alias visit_range copy_and_visit_children
144
+ alias visit_rassign copy_and_visit_children
145
+ alias visit_rational copy_and_visit_children
146
+ alias visit_rbrace copy_and_visit_children
147
+ alias visit_rbracket copy_and_visit_children
148
+ alias visit_redo copy_and_visit_children
149
+ alias visit_regexp_beg copy_and_visit_children
150
+ alias visit_regexp_content copy_and_visit_children
151
+ alias visit_regexp_end copy_and_visit_children
152
+ alias visit_regexp_literal copy_and_visit_children
153
+ alias visit_rescue copy_and_visit_children
154
+ alias visit_rescue_ex copy_and_visit_children
155
+ alias visit_rescue_mod copy_and_visit_children
156
+ alias visit_rest_param copy_and_visit_children
157
+ alias visit_retry copy_and_visit_children
158
+ alias visit_return copy_and_visit_children
159
+ alias visit_rparen copy_and_visit_children
160
+ alias visit_sclass copy_and_visit_children
161
+ alias visit_statements copy_and_visit_children
162
+ alias visit_string_concat copy_and_visit_children
163
+ alias visit_string_content copy_and_visit_children
164
+ alias visit_string_dvar copy_and_visit_children
165
+ alias visit_string_embexpr copy_and_visit_children
166
+ alias visit_string_literal copy_and_visit_children
167
+ alias visit_super copy_and_visit_children
168
+ alias visit_symbeg copy_and_visit_children
169
+ alias visit_symbol_content copy_and_visit_children
170
+ alias visit_symbol_literal copy_and_visit_children
171
+ alias visit_symbols copy_and_visit_children
172
+ alias visit_symbols_beg copy_and_visit_children
173
+ alias visit_tlambda copy_and_visit_children
174
+ alias visit_tlambeg copy_and_visit_children
175
+ alias visit_top_const_field copy_and_visit_children
176
+ alias visit_top_const_ref copy_and_visit_children
177
+ alias visit_tstring_beg copy_and_visit_children
178
+ alias visit_tstring_content copy_and_visit_children
179
+ alias visit_tstring_end copy_and_visit_children
180
+ alias visit_unary copy_and_visit_children
181
+ alias visit_undef copy_and_visit_children
182
+ alias visit_unless copy_and_visit_children
183
+ alias visit_until copy_and_visit_children
184
+ alias visit_var_field copy_and_visit_children
185
+ alias visit_var_ref copy_and_visit_children
186
+ alias visit_vcall copy_and_visit_children
187
+ alias visit_void_stmt copy_and_visit_children
188
+ alias visit_when copy_and_visit_children
189
+ alias visit_while copy_and_visit_children
190
+ alias visit_word copy_and_visit_children
191
+ alias visit_words copy_and_visit_children
192
+ alias visit_words_beg copy_and_visit_children
193
+ alias visit_xstring copy_and_visit_children
194
+ alias visit_xstring_literal copy_and_visit_children
195
+ alias visit_yield copy_and_visit_children
196
+ alias visit_zsuper copy_and_visit_children
197
+ alias visit___end__ copy_and_visit_children
198
+ end
199
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module ERB
5
+ # A base ERB Visitor class that returns a deep copy of the AST.
6
+ # It can be subclassed to mutate some parts of the AST and keep everything
7
+ # else as it was.
8
+ #
9
+ # Ideally this class (or equivalent) would exist within SyntaxTree::ERB gem.
10
+ class MutationVisitor
11
+ def visit(node)
12
+ node&.accept(self)
13
+ end
14
+
15
+ def copy_and_visit_children(node)
16
+ node.dup.tap do |clone|
17
+ clone.instance_variables.each do |name|
18
+ var = clone.instance_variable_get(name)
19
+ clone.instance_variable_set(
20
+ name,
21
+ case var
22
+ when Array
23
+ var.map { |child| visit(child) }
24
+ when SyntaxTree::ERB::Node
25
+ visit(var)
26
+ else
27
+ var
28
+ end
29
+ )
30
+ end
31
+ end
32
+ end
33
+
34
+ alias visit_attribute copy_and_visit_children
35
+ alias visit_block copy_and_visit_children
36
+ alias visit_char_data copy_and_visit_children
37
+ alias visit_closing_tag copy_and_visit_children
38
+ alias visit_doctype copy_and_visit_children
39
+ alias visit_document copy_and_visit_children
40
+ alias visit_erb copy_and_visit_children
41
+ alias visit_erb_block copy_and_visit_children
42
+ alias visit_erb_case copy_and_visit_children
43
+ alias visit_erb_case_when copy_and_visit_children
44
+ alias visit_erb_close copy_and_visit_children
45
+ alias visit_erb_comment copy_and_visit_children
46
+ alias visit_erb_content copy_and_visit_children
47
+ alias visit_erb_do_close copy_and_visit_children
48
+ alias visit_erb_else copy_and_visit_children
49
+ alias visit_erb_end copy_and_visit_children
50
+ alias visit_erb_if copy_and_visit_children
51
+ alias visit_erb_yield copy_and_visit_children
52
+ alias visit_html copy_and_visit_children
53
+ alias visit_html_comment copy_and_visit_children
54
+ alias visit_html_string copy_and_visit_children
55
+ alias visit_new_line copy_and_visit_children
56
+ alias visit_opening_tag copy_and_visit_children
57
+ alias visit_token copy_and_visit_children
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module Haml
5
+ class MutationVisitor
6
+ # A base Haml Visitor class that returns a deep copy of the AST.
7
+ # It can be subclassed to mutate some parts of the AST and keep everything
8
+ # else as it was.
9
+ #
10
+ # Ideally this class (or equivalent) would existing within SyntaxTree::Haml gem.
11
+ def visit(node)
12
+ node&.accept(self)
13
+ end
14
+
15
+ def copy_and_visit_children(node)
16
+ # Haml::Parser::ParseNode has these main attributes
17
+ # - value: nil | Hash
18
+ # - children: [ParseNode]
19
+ # - parent: ParseNode
20
+ node.dup.tap do |clone|
21
+ clone.value = node.value.dup unless node.value.nil?
22
+ clone.children =
23
+ node.children.map do |child|
24
+ visit(child).tap { |child_copy| child_copy.parent = clone }
25
+ end
26
+ end
27
+ end
28
+
29
+ alias visit_comment copy_and_visit_children
30
+ alias visit_doctype copy_and_visit_children
31
+ alias visit_filter copy_and_visit_children
32
+ alias visit_haml_comment copy_and_visit_children
33
+ alias visit_plain copy_and_visit_children
34
+ alias visit_root copy_and_visit_children
35
+ alias visit_script copy_and_visit_children
36
+ alias visit_silent_script copy_and_visit_children
37
+ alias visit_tag copy_and_visit_children
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree/erb/mutation_visitor"
4
+
5
+ module SyntaxTree
6
+ module Tailwindcss
7
+ class ErbMutationVisitor < SyntaxTree::ERB::MutationVisitor
8
+ def initialize(sorter)
9
+ super()
10
+ @sorter = sorter
11
+ @ruby_visitor = SyntaxTree::Tailwindcss::RubyMutationVisitor.new(sorter)
12
+ end
13
+
14
+ # Rewrite `<%= tag.span class: 'foo bar' %>`
15
+ def visit_erb_content(node)
16
+ node.dup.tap do |clone|
17
+ clone.instance_variable_set(:@value, clone.value.accept(@ruby_visitor))
18
+ end
19
+ end
20
+
21
+ # Rewrite `class="foo <%= something %> bar"`
22
+ def visit_attribute(node)
23
+ clone = super
24
+ return clone unless node.key.value == "class"
25
+ return clone unless node.value.is_a?(SyntaxTree::ERB::HtmlString)
26
+
27
+ contents =
28
+ clone.value.contents.reject do |content|
29
+ content.is_a?(SyntaxTree::ERB::Token) && content.type == :whitespace
30
+ end
31
+
32
+ clone.value.instance_variable_set(
33
+ :@contents,
34
+ contents.map.with_index do |content, index|
35
+ next content unless content.is_a?(SyntaxTree::ERB::Token)
36
+
37
+ # It's already a clone, so it's OK to mutate it
38
+ new_value = @sorter.sort(content.value.split).join(" ")
39
+ new_value = " #{new_value}" if index.positive? && erb_node?(contents[index - 1])
40
+ new_value = "#{new_value} " if index < contents.size - 1 &&
41
+ erb_node?(contents[index + 1])
42
+ content.instance_variable_set(:@value, new_value)
43
+ content
44
+ end
45
+ )
46
+
47
+ clone
48
+ end
49
+
50
+ private
51
+
52
+ def erb_node?(node)
53
+ node.is_a?(SyntaxTree::ERB::ErbNode)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree/haml/mutation_visitor"
4
+
5
+ module SyntaxTree
6
+ module Tailwindcss
7
+ class HamlMutationVisitor < SyntaxTree::Haml::MutationVisitor
8
+ def initialize(sorter)
9
+ super()
10
+ @sorter = sorter
11
+ @ruby_visitor = SyntaxTree::Tailwindcss::RubyMutationVisitor.new(@sorter)
12
+ end
13
+
14
+ def visit_tag(node)
15
+ super.tap do |clone|
16
+ next unless node.value
17
+
18
+ # Rewrite `.foo.bar`
19
+ if node.value[:attributes].key?("class")
20
+ clone.value[:attributes]["class"] = @sorter.sort(
21
+ node.value[:attributes]["class"].split
22
+ ).join(" ")
23
+ end
24
+
25
+ # Rewrite `(class="foo #{something} bar")`
26
+ if node.value[:dynamic_attributes].new&.include?('"class"')
27
+ clone.value[:dynamic_attributes] = node.value[:dynamic_attributes].dup.tap do |attrs|
28
+ attrs.new = rewrite_html_attributes(attrs.new)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # Rewrites `(class="foo #{something} bar")`
35
+ def rewrite_html_attributes(string)
36
+ new_string = string.dup
37
+
38
+ pattern = <<~PATTERN
39
+ Assoc[
40
+ key: StringLiteral[parts: [TStringContent[value: "class"]]],
41
+ value: StringLiteral
42
+ ]
43
+ PATTERN
44
+
45
+ SyntaxTree.search(string, pattern) do |assoc_node|
46
+ location = assoc_node.value.location
47
+ rewritten = @ruby_visitor.rewrite_string_literal(assoc_node.value)
48
+
49
+ formatter = SyntaxTree::Formatter.new(string, [], 10_000)
50
+ rewritten.format(formatter)
51
+ formatter.flush(0)
52
+ formatted = formatter.output.join
53
+
54
+ new_string[location.start_char...location.end_char] = formatted
55
+ end
56
+
57
+ new_string
58
+ end
59
+
60
+ def haml_attributes_mutation_visitor
61
+ @haml_attributes_mutation_visitor ||=
62
+ SyntaxTree.mutation do |visitor|
63
+ # Rewrite `class: ["foo", "bar"]` and `class: "foo bar"`
64
+ visitor.mutate(<<~PATTERN) do |node|
65
+ Assoc[
66
+ key: Label[value: 'class:'] | SymbolLiteral[value: Kw[value: 'class']],
67
+ value: ArrayLiteral[contents: Args] | StringLiteral
68
+ ]
69
+ PATTERN
70
+ case node.value
71
+ when ArrayLiteral
72
+ args = @ruby_visitor.rewrite_args(node.value.contents)
73
+ node.copy(value: node.value.copy(contents: args))
74
+ when StringLiteral
75
+ new_value = @ruby_visitor.rewrite_string_literal(node.value)
76
+ node.copy(value: new_value)
77
+ else
78
+ node
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module Tailwindcss
5
+ module Patches
6
+ module SyntaxTree
7
+ def format_node(source, node, ...)
8
+ modified_node = node.accept(Tailwindcss.ruby_mutation_visitor)
9
+ super(source, modified_node, ...)
10
+ end
11
+ end
12
+
13
+ module ERB
14
+ def parse(source)
15
+ node = ::SyntaxTree::ERB::Parser.new(source).parse
16
+ node.accept(Tailwindcss.erb_mutation_visitor)
17
+ end
18
+ end
19
+
20
+ module Haml
21
+ def parse(source)
22
+ node = ::Haml::Parser.new({}).call(source)
23
+ node.accept(Tailwindcss.haml_mutation_visitor)
24
+ end
25
+ end
26
+
27
+ module HamlFormat
28
+ def parse_attributes_hash(source, node, ...)
29
+ modified_node =
30
+ node.accept(Tailwindcss.haml_mutation_visitor.haml_attributes_mutation_visitor)
31
+ super(source, modified_node, ...)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ class << self
38
+ prepend Tailwindcss::Patches::SyntaxTree
39
+ end
40
+
41
+ # This should only be done if ERB plugin is already loaded
42
+ if const_defined?("ERB")
43
+ module ERB
44
+ class << self
45
+ prepend Tailwindcss::Patches::ERB
46
+ end
47
+ end
48
+ end
49
+
50
+ # This should only be done if Haml plugin is already loaded
51
+ if const_defined?("Haml")
52
+ module Haml
53
+ class << self
54
+ prepend Tailwindcss::Patches::Haml
55
+ end
56
+ Format.prepend(Tailwindcss::Patches::HamlFormat)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "syntax_tree/complete_mutation_visitor"
4
+
5
+ module SyntaxTree
6
+ module Tailwindcss
7
+ class RubyMutationVisitor < SyntaxTree::CompleteMutationVisitor
8
+ def initialize(sorter)
9
+ super()
10
+ @sorter = sorter
11
+
12
+ # Rewrite `class: "foo bar"` and `:class => "foo bar"`
13
+ pattern = <<~PATTERN
14
+ Assoc[
15
+ key: Label[value: 'class:'] | SymbolLiteral[value: Kw[value: 'class']],
16
+ value: StringLiteral
17
+ ]
18
+ PATTERN
19
+ mutate(pattern) { |node| node.copy(value: rewrite_string_literal(node.value)) }
20
+
21
+ # Rewrite `class_names("foo bar", "lorem ipsum")`
22
+ mutate("CallNode[message: Ident[value: 'class_names']]") do |node|
23
+ next node unless node.arguments.is_a?(ArgParen)
24
+ next node unless node.arguments.arguments.is_a?(Args)
25
+
26
+ args = node.arguments.arguments
27
+ node.copy(arguments: node.arguments.copy(arguments: rewrite_args(args)))
28
+ end
29
+
30
+ # Rewrite `class_names "foo bar", "lorem ipsum"`
31
+ mutate("Command[message: Ident[value: 'class_names']]") do |node|
32
+ next node unless node.arguments.is_a?(Args)
33
+
34
+ node.copy(arguments: rewrite_args(node.arguments))
35
+ end
36
+ end
37
+
38
+ def rewrite_string_literal(string_literal)
39
+ return string_literal unless string_literal.is_a?(StringLiteral)
40
+
41
+ string_parts =
42
+ string_literal.parts.map do |string_part|
43
+ next string_part unless string_part.is_a?(TStringContent)
44
+
45
+ value = string_part.value
46
+ new_value = @sorter.sort(value.split).join(" ")
47
+ new_value = " #{new_value}" if value.match?(/\A\s/)
48
+ new_value = "#{new_value} " if value.match?(/\s\z/)
49
+ string_part.copy(value: new_value)
50
+ end
51
+ string_literal.copy(parts: string_parts)
52
+ end
53
+
54
+ def rewrite_args(args)
55
+ return args unless args.is_a?(Args)
56
+
57
+ # Rewrite each string literal separately, e.g. 'mb-4 text-2xl'
58
+ rewritten_parts = args.parts.map { |part| rewrite_string_literal(part) }
59
+
60
+ # Reorder subsequent single-class strings, e.g. 'mb-4', 'text-2xl'
61
+ reordered_parts =
62
+ rewritten_parts
63
+ .slice_when { |a, b| single_class_string_literal?(a) ^ single_class_string_literal?(b) }
64
+ .map do |segment|
65
+ next segment if segment.size == 1 || !single_class_string_literal?(segment.first)
66
+
67
+ segment
68
+ .uniq { |node| node.parts.first.value }
69
+ .sort_by { |node| @sorter.sort_order(node.parts.first.value) }
70
+ end
71
+ .flatten
72
+
73
+ args.copy(parts: reordered_parts)
74
+ end
75
+
76
+ def single_class_string_literal?(string_literal)
77
+ string_literal.is_a?(StringLiteral) && string_literal.parts.size == 1 &&
78
+ string_literal.parts.first.is_a?(TStringContent) &&
79
+ string_literal.parts.first.value.match?(/\A\S+\z/)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module Tailwindcss
5
+ class Sorter
6
+ def initialize(classes_in_order)
7
+ @classes_order = classes_in_order.to_enum.with_index.to_h
8
+ end
9
+
10
+ def sort(classes)
11
+ classes.uniq.sort_by { |cls| @classes_order[cls] || -1 }
12
+ end
13
+
14
+ def sort_order(cls)
15
+ @classes_order[cls] || -1
16
+ end
17
+
18
+ class << self
19
+ def load_cached
20
+ return new(SyntaxTree::Tailwindcss.custom_order) if SyntaxTree::Tailwindcss.custom_order
21
+
22
+ mtime = File.mtime(tailwind_output_path)
23
+ return @cached_sorter if @cached_mtime == mtime
24
+
25
+ @cached_sorter = load!
26
+ @cached_mtime = mtime
27
+ @cached_sorter
28
+ rescue Errno::ENOENT
29
+ warn do
30
+ "Couldn't find TailwindCSS output (#{tailwind_output_path}). Classes won't be sorted."
31
+ end
32
+ new([])
33
+ end
34
+
35
+ def load!
36
+ tailwind_output = File.read(tailwind_output_path)
37
+ classes = parse_tailwind_output(tailwind_output)
38
+ new(classes)
39
+ end
40
+
41
+ def tailwind_output_path
42
+ if SyntaxTree::Tailwindcss.output_path
43
+ SyntaxTree::Tailwindcss.output_path
44
+ elsif ENV["TAILWIND_OUTPUT_PATH"]
45
+ ENV["TAILWIND_OUTPUT_PATH"]
46
+ else
47
+ "app/assets/builds/application.css"
48
+ end
49
+ end
50
+
51
+ def parse_tailwind_output(output)
52
+ # We could use a real CSS parser, but that'd be slow and we only need to know valid classes
53
+ output
54
+ .gsub(%r{/\*.+?\*/}, "")
55
+ .gsub(/\\(.)/) { "-ESCAPED-#{Regexp.last_match[1].ord}-" }
56
+ .scan(/(?<=\.)[^0-9][^. {:>),\[]*/)
57
+ .uniq
58
+ .join(" ")
59
+ .gsub(/-ESCAPED-(\d+)-/) { Regexp.last_match[1].to_i.chr(Encoding::UTF_8) }
60
+ .split
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ module Tailwindcss
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tailwindcss/version"
4
+ require_relative "tailwindcss/patches"
5
+
6
+ module SyntaxTree
7
+ module Tailwindcss
8
+ autoload :Sorter, "syntax_tree/tailwindcss/sorter"
9
+ autoload :RubyMutationVisitor, "syntax_tree/tailwindcss/ruby_mutation_visitor"
10
+ autoload :ErbMutationVisitor, "syntax_tree/tailwindcss/erb_mutation_visitor"
11
+ autoload :HamlMutationVisitor, "syntax_tree/tailwindcss/haml_mutation_visitor"
12
+
13
+ class << self
14
+ attr_accessor :output_path, :custom_order
15
+
16
+ def ruby_mutation_visitor
17
+ sorter = Sorter.load_cached
18
+ RubyMutationVisitor.new(sorter)
19
+ end
20
+
21
+ def erb_mutation_visitor
22
+ sorter = Sorter.load_cached
23
+ ErbMutationVisitor.new(sorter)
24
+ end
25
+
26
+ def haml_mutation_visitor
27
+ sorter = Sorter.load_cached
28
+ HamlMutationVisitor.new(sorter)
29
+ end
30
+ end
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: syntax_tree-tailwindcss
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tomasz Szczęśniak-Szlagowski
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: syntax_tree
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.2'
27
+ description: It sorts Tailwind classes in your helpers and ERB files and sorts them
28
+ email:
29
+ - spect88@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".rubocop.yml"
35
+ - ".streerc"
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - lib/syntax_tree/complete_mutation_visitor.rb
40
+ - lib/syntax_tree/erb/mutation_visitor.rb
41
+ - lib/syntax_tree/haml/mutation_visitor.rb
42
+ - lib/syntax_tree/tailwindcss.rb
43
+ - lib/syntax_tree/tailwindcss/erb_mutation_visitor.rb
44
+ - lib/syntax_tree/tailwindcss/haml_mutation_visitor.rb
45
+ - lib/syntax_tree/tailwindcss/patches.rb
46
+ - lib/syntax_tree/tailwindcss/ruby_mutation_visitor.rb
47
+ - lib/syntax_tree/tailwindcss/sorter.rb
48
+ - lib/syntax_tree/tailwindcss/version.rb
49
+ homepage: https://github.com/spect88/syntax_tree-tailwindcss
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/spect88/syntax_tree-tailwindcss
54
+ source_code_uri: https://github.com/spect88/syntax_tree-tailwindcss
55
+ rubygems_mfa_required: 'true'
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 3.0.0
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.3.7
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: A Syntax Tree plugin for sorting TailwindCSS classes
75
+ test_files: []