phlexing 0.2.0 → 0.4.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 +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +167 -15
- data/Rakefile +1 -6
- data/lib/phlexing/component_generator.rb +121 -0
- data/lib/phlexing/converter.rb +19 -178
- data/lib/phlexing/erb_transformer.rb +46 -0
- data/lib/phlexing/formatter.rb +22 -0
- data/lib/phlexing/helpers.rb +85 -55
- data/lib/phlexing/minifier.rb +41 -0
- data/lib/phlexing/name_suggestor.rb +76 -0
- data/lib/phlexing/options.rb +27 -0
- data/lib/phlexing/parser.rb +32 -0
- data/lib/phlexing/patches/html_press.rb +28 -0
- data/lib/phlexing/refinements/string_refinements.rb +26 -0
- data/lib/phlexing/ruby_analyzer.rb +83 -0
- data/lib/phlexing/template_generator.rb +209 -0
- data/lib/phlexing/version.rb +1 -1
- data/lib/phlexing/visitor.rb +105 -0
- data/lib/phlexing.rb +14 -1
- metadata +47 -8
data/lib/phlexing/helpers.rb
CHANGED
@@ -1,102 +1,132 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "phlex"
|
4
|
+
require "phlex-rails"
|
5
|
+
|
3
6
|
module Phlexing
|
4
7
|
module Helpers
|
5
|
-
|
6
|
-
|
8
|
+
KNOWN_ELEMENTS =
|
9
|
+
Phlex::HTML::VoidElements.registered_elements.values +
|
10
|
+
Phlex::HTML::StandardElements.registered_elements.values
|
7
11
|
|
8
|
-
|
12
|
+
def whitespace
|
13
|
+
options.whitespace? ? "whitespace\n" : ""
|
9
14
|
end
|
10
15
|
|
11
|
-
def
|
12
|
-
|
16
|
+
def newline
|
17
|
+
"\n"
|
13
18
|
end
|
14
19
|
|
15
|
-
def
|
16
|
-
"
|
20
|
+
def symbol(string)
|
21
|
+
":#{string}"
|
17
22
|
end
|
18
23
|
|
19
|
-
def
|
20
|
-
"
|
24
|
+
def arg(string)
|
25
|
+
"#{string}: "
|
21
26
|
end
|
22
27
|
|
23
|
-
def
|
28
|
+
def quote(string)
|
24
29
|
"%(#{string})"
|
25
30
|
end
|
26
31
|
|
27
|
-
def
|
28
|
-
|
29
|
-
return single_quote(string) unless string.include?("'")
|
30
|
-
|
31
|
-
percent_literal_string(string)
|
32
|
+
def parens(string)
|
33
|
+
"(#{string})"
|
32
34
|
end
|
33
35
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
name = node.name.gsub("-", "_")
|
39
|
-
@custom_elements << name
|
36
|
+
def braces(string)
|
37
|
+
"{ #{string} }"
|
38
|
+
end
|
40
39
|
|
41
|
-
|
40
|
+
def interpolate(string)
|
41
|
+
"\#\{#{string}\}"
|
42
42
|
end
|
43
43
|
|
44
|
-
def
|
45
|
-
|
44
|
+
def unescape(source)
|
45
|
+
CGI.unescapeHTML(source)
|
46
46
|
end
|
47
47
|
|
48
|
-
def
|
49
|
-
|
48
|
+
def unwrap_erb(source)
|
49
|
+
source
|
50
|
+
.delete_prefix("<%==")
|
51
|
+
.delete_prefix("<%=")
|
52
|
+
.delete_prefix("<%-")
|
53
|
+
.delete_prefix("<%#")
|
54
|
+
.delete_prefix("<% #")
|
55
|
+
.delete_prefix("<%")
|
56
|
+
.delete_suffix("-%>")
|
57
|
+
.delete_suffix("%>")
|
58
|
+
.strip
|
50
59
|
end
|
51
60
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
61
|
+
def tag_name(node)
|
62
|
+
return "template_tag" if node.name == "template-tag"
|
63
|
+
|
64
|
+
name = node.name.tr("-", "_")
|
65
|
+
|
66
|
+
@converter.custom_elements << name unless KNOWN_ELEMENTS.include?(name)
|
67
|
+
|
68
|
+
name
|
56
69
|
end
|
57
70
|
|
58
|
-
def
|
59
|
-
|
71
|
+
def block
|
72
|
+
out << " {"
|
60
73
|
yield
|
61
|
-
|
74
|
+
out << " }"
|
62
75
|
end
|
63
76
|
|
64
|
-
def
|
65
|
-
|
77
|
+
def output(name, string)
|
78
|
+
out << name
|
79
|
+
out << " "
|
80
|
+
out << string.strip
|
81
|
+
out << newline
|
66
82
|
end
|
67
83
|
|
68
|
-
def
|
69
|
-
|
84
|
+
def blocklist
|
85
|
+
[
|
86
|
+
"render"
|
87
|
+
]
|
70
88
|
end
|
71
89
|
|
72
|
-
def
|
73
|
-
|
90
|
+
def routes_helpers
|
91
|
+
[
|
92
|
+
/\w+_url/,
|
93
|
+
/\w+_path/
|
94
|
+
]
|
74
95
|
end
|
75
96
|
|
76
|
-
def
|
77
|
-
|
97
|
+
def known_rails_helpers
|
98
|
+
Phlex::Rails::Helpers
|
99
|
+
.constants
|
100
|
+
.reject { |m| m == :Routes }
|
101
|
+
.map { |m| Module.const_get("::Phlex::Rails::Helpers::#{m}") }
|
102
|
+
.each_with_object({}) { |m, sum|
|
103
|
+
(m.instance_methods - Module.instance_methods).each do |method|
|
104
|
+
sum[method.to_s] = m.name
|
105
|
+
end
|
106
|
+
}
|
78
107
|
end
|
79
108
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
109
|
+
def string_output?(node)
|
110
|
+
word = node.text.strip.scan(/^\w+/)[0]
|
111
|
+
|
112
|
+
return true if word.nil?
|
83
113
|
|
84
|
-
|
85
|
-
|
114
|
+
blocklist_matched = known_rails_helpers.keys.include?(word) || blocklist.include?(word)
|
115
|
+
route_matched = routes_helpers.map { |regex| word.scan(regex).any? }.reduce(:|)
|
86
116
|
|
87
|
-
|
88
|
-
node.children.one? &&
|
89
|
-
first.children.none? &&
|
90
|
-
text_node?(first) &&
|
91
|
-
node.attributes["interpolated"]
|
117
|
+
!(blocklist_matched || route_matched)
|
92
118
|
end
|
93
119
|
|
94
|
-
def
|
95
|
-
|
120
|
+
def children?(node)
|
121
|
+
node.children.length >= 1
|
96
122
|
end
|
97
123
|
|
98
|
-
def
|
99
|
-
node.
|
124
|
+
def multiple_children?(node)
|
125
|
+
node.children.length > 1
|
126
|
+
end
|
127
|
+
|
128
|
+
def siblings?(node)
|
129
|
+
multiple_children?(node.parent)
|
100
130
|
end
|
101
131
|
end
|
102
132
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "html_press"
|
4
|
+
|
5
|
+
module Phlexing
|
6
|
+
class Minifier
|
7
|
+
def self.call(...)
|
8
|
+
new(...).call
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(source)
|
12
|
+
@source = source.to_s.dup
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
minify
|
17
|
+
minify_html_entities
|
18
|
+
|
19
|
+
@source
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def minify
|
25
|
+
@source = HtmlPress.press(@source)
|
26
|
+
rescue StandardError
|
27
|
+
@source
|
28
|
+
end
|
29
|
+
|
30
|
+
def minify_html_entities
|
31
|
+
@source = @source
|
32
|
+
.gsub("& lt;", "<")
|
33
|
+
.gsub("& quot;", """)
|
34
|
+
.gsub("& gt;", ">")
|
35
|
+
.gsub("& #amp;", "&#amp;")
|
36
|
+
.gsub("& #38;", "&")
|
37
|
+
.gsub("& #60;", "<")
|
38
|
+
.gsub("& #62;", ">")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlexing
|
4
|
+
class NameSuggestor
|
5
|
+
using Refinements::StringRefinements
|
6
|
+
|
7
|
+
def self.call(source)
|
8
|
+
document = Parser.call(source)
|
9
|
+
analyzer = RubyAnalyzer.call(source)
|
10
|
+
|
11
|
+
ivars = analyzer.ivars
|
12
|
+
locals = analyzer.locals
|
13
|
+
|
14
|
+
ids = extract(document, :extract_id_from_element)
|
15
|
+
classes = extract(document, :extract_class_from_element)
|
16
|
+
tags = extract(document, :extract_tag_name_from_element)
|
17
|
+
|
18
|
+
return wrap(ivars.first) if ivars.one? && locals.none?
|
19
|
+
return wrap(locals.first) if locals.one? && ivars.none?
|
20
|
+
return wrap(ids.first) if ids.any?
|
21
|
+
return wrap(ivars.first) if ivars.any?
|
22
|
+
return wrap(locals.first) if locals.any?
|
23
|
+
return wrap(classes.first) if classes.any?
|
24
|
+
return wrap(tags.first) if tags.any?
|
25
|
+
|
26
|
+
"Component"
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.wrap(name)
|
30
|
+
"#{name}_component".underscore.camelize
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.extract(document, method)
|
34
|
+
return [] unless document
|
35
|
+
|
36
|
+
document.children.map { |element| send(method, element) }.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.extract_id_from_element(element)
|
40
|
+
return if element.nil?
|
41
|
+
return if element.is_a?(Nokogiri::XML::Text)
|
42
|
+
|
43
|
+
id_attribute = element.attributes && element.attributes["id"]
|
44
|
+
return if id_attribute.nil?
|
45
|
+
|
46
|
+
id = id_attribute.value.to_s.strip
|
47
|
+
return if id.include?("<erb")
|
48
|
+
|
49
|
+
id
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.extract_class_from_element(element)
|
53
|
+
return if element.nil?
|
54
|
+
return if element.is_a?(Nokogiri::XML::Text)
|
55
|
+
|
56
|
+
class_attribute = element.attributes && element.attributes["class"]
|
57
|
+
|
58
|
+
return if class_attribute.nil?
|
59
|
+
|
60
|
+
classes = class_attribute.value.strip.split
|
61
|
+
|
62
|
+
return if classes.empty?
|
63
|
+
|
64
|
+
classes[0]
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.extract_tag_name_from_element(element)
|
68
|
+
return if element.nil?
|
69
|
+
return if element.is_a?(Nokogiri::XML::Text)
|
70
|
+
|
71
|
+
return if ["div", "span", "p", "erb"].include?(element.name)
|
72
|
+
|
73
|
+
element.name
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Phlexing
|
4
|
+
class Options
|
5
|
+
attr_accessor :component, :component_name, :parent_component, :whitespace
|
6
|
+
|
7
|
+
alias_method :whitespace?, :whitespace
|
8
|
+
alias_method :component?, :component
|
9
|
+
|
10
|
+
def initialize(component: false, component_name: "Component", parent_component: "Phlex::HTML", whitespace: true)
|
11
|
+
@component = component
|
12
|
+
@component_name = safe_constant_name(component_name)
|
13
|
+
@parent_component = safe_constant_name(parent_component)
|
14
|
+
@whitespace = whitespace
|
15
|
+
end
|
16
|
+
|
17
|
+
def safe_constant_name(name)
|
18
|
+
name = name.to_s
|
19
|
+
|
20
|
+
if name[0] == "0" || name[0].to_i != 0
|
21
|
+
"A#{name}"
|
22
|
+
else
|
23
|
+
name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
module Phlexing
|
6
|
+
class Parser
|
7
|
+
def self.call(source)
|
8
|
+
source = ERBTransformer.call(source)
|
9
|
+
source = Minifier.call(source)
|
10
|
+
|
11
|
+
# Credit:
|
12
|
+
# https://github.com/spree/deface/blob/6bf18df76715ee3eb3d0cd1b6eda822817ace91c/lib/deface/parser.rb#L105-L111
|
13
|
+
#
|
14
|
+
|
15
|
+
html_tag = /<html(( .*?(?:(?!>)[\s\S])*>)|>)/i
|
16
|
+
head_tag = /<head(( .*?(?:(?!>)[\s\S])*>)|>)/i
|
17
|
+
body_tag = /<body(( .*?(?:(?!>)[\s\S])*>)|>)/i
|
18
|
+
|
19
|
+
if source =~ html_tag
|
20
|
+
Nokogiri::HTML::Document.parse(source)
|
21
|
+
elsif source =~ head_tag && source =~ body_tag
|
22
|
+
Nokogiri::HTML::Document.parse(source).css("html").first
|
23
|
+
elsif source =~ head_tag
|
24
|
+
Nokogiri::HTML::Document.parse(source).css("head").first
|
25
|
+
elsif source =~ body_tag
|
26
|
+
Nokogiri::HTML::Document.parse(source).css("body").first
|
27
|
+
else
|
28
|
+
Nokogiri::HTML::DocumentFragment.parse(source)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "html_press"
|
4
|
+
|
5
|
+
module HtmlPress
|
6
|
+
class Html
|
7
|
+
# We want to preserve HTML comments in the minification step
|
8
|
+
# so we can output them again in the phlex template
|
9
|
+
def process_html_comments(out)
|
10
|
+
out
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Entities
|
15
|
+
# The minification step turned this input
|
16
|
+
# <div data-erb-class="<%= something? ? "class-1" : "class-2" %>">Text</div>
|
17
|
+
#
|
18
|
+
# into this output:
|
19
|
+
# <div data-erb-class="<%= something? ? " class-1" :" class-2" %& gt;">Text</div>
|
20
|
+
#
|
21
|
+
# which in our wasn't ideal, because nokogiri parsed it as:
|
22
|
+
# <div data-erb-class="<%= something? ? " class-1=" :" class-2="%>">Text</div>
|
23
|
+
#
|
24
|
+
def minify(out)
|
25
|
+
out
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -4,15 +4,41 @@ module Phlexing
|
|
4
4
|
module Refinements
|
5
5
|
module StringRefinements
|
6
6
|
refine String do
|
7
|
+
# https://github.com/rails/rails/blob/46c45935123e7ae003767900e7d22a6e41995701/activesupport/lib/active_support/core_ext/string/access.rb#L46-L48
|
8
|
+
def from(position)
|
9
|
+
self[position, length]
|
10
|
+
end
|
11
|
+
|
12
|
+
# https://github.com/rails/rails/blob/46c45935123e7ae003767900e7d22a6e41995701/activesupport/lib/active_support/core_ext/string/access.rb#L63-L66 def from(position)
|
13
|
+
def to(position)
|
14
|
+
position += size if position < 0
|
15
|
+
self[0, position + 1] || +""
|
16
|
+
end
|
17
|
+
|
18
|
+
# https://github.com/rails/rails/blob/46c45935123e7ae003767900e7d22a6e41995701/activesupport/lib/active_support/core_ext/string/filters.rb#L13-L15
|
7
19
|
def squish
|
8
20
|
dup.squish!
|
9
21
|
end
|
10
22
|
|
23
|
+
# https://github.com/rails/rails/blob/46c45935123e7ae003767900e7d22a6e41995701/activesupport/lib/active_support/core_ext/string/filters.rb#L21-L25
|
11
24
|
def squish!
|
12
25
|
gsub!(/[[:space:]]+/, " ")
|
13
26
|
strip!
|
14
27
|
self
|
15
28
|
end
|
29
|
+
|
30
|
+
# https://stackoverflow.com/questions/4072159/classify-a-ruby-string#comment4378937_4072202
|
31
|
+
def camelize
|
32
|
+
split("_").collect(&:capitalize).join
|
33
|
+
end
|
34
|
+
|
35
|
+
def dasherize
|
36
|
+
tr("_", "-").tr(" ", "-")
|
37
|
+
end
|
38
|
+
|
39
|
+
def underscore
|
40
|
+
tr("-", "_").tr(" ", "_")
|
41
|
+
end
|
16
42
|
end
|
17
43
|
end
|
18
44
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "syntax_tree"
|
4
|
+
|
5
|
+
module Phlexing
|
6
|
+
class RubyAnalyzer
|
7
|
+
attr_accessor :ivars, :locals, :idents, :calls, :consts, :instance_methods, :includes
|
8
|
+
|
9
|
+
def self.call(source)
|
10
|
+
new.analyze(source)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@ivars = Set.new
|
15
|
+
@locals = Set.new
|
16
|
+
@idents = Set.new
|
17
|
+
@calls = Set.new
|
18
|
+
@consts = Set.new
|
19
|
+
@instance_methods = Set.new
|
20
|
+
@includes = Set.new
|
21
|
+
@visitor = Visitor.new(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def analyze(source)
|
25
|
+
code = extract_ruby_from_erb(source.to_s)
|
26
|
+
|
27
|
+
analyze_ruby(code)
|
28
|
+
end
|
29
|
+
|
30
|
+
def analyze_ruby(code)
|
31
|
+
program = SyntaxTree.parse(code)
|
32
|
+
@visitor.visit(program)
|
33
|
+
|
34
|
+
self
|
35
|
+
rescue SyntaxTree::Parser::ParseError, NoMethodError
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def extract_ruby_from_erb(source)
|
42
|
+
document = Parser.call(source)
|
43
|
+
lines = []
|
44
|
+
|
45
|
+
lines << ruby_lines_from_erb_tags(document)
|
46
|
+
lines << ruby_lines_from_erb_attributes(document)
|
47
|
+
|
48
|
+
lines.join("\n")
|
49
|
+
rescue StandardError
|
50
|
+
""
|
51
|
+
end
|
52
|
+
|
53
|
+
def ruby_lines_from_erb_tags(document)
|
54
|
+
nodes = document.css("erb")
|
55
|
+
|
56
|
+
nodes
|
57
|
+
.map { |node| node.text.to_s.strip }
|
58
|
+
.map { |line| line.delete_prefix("=") }
|
59
|
+
.map { |line| line.delete_prefix("-") }
|
60
|
+
.map { |line| line.delete_suffix("-") }
|
61
|
+
end
|
62
|
+
|
63
|
+
def ruby_lines_from_erb_attributes(document)
|
64
|
+
attributes = document.css("*").map(&:attributes)
|
65
|
+
|
66
|
+
lines = []
|
67
|
+
|
68
|
+
attributes.each do |pair|
|
69
|
+
pair.select! { |name, _| name.start_with?("data-erb-") }
|
70
|
+
|
71
|
+
pair.each do |_, value|
|
72
|
+
Parser
|
73
|
+
.call(value)
|
74
|
+
.children
|
75
|
+
.select { |child| child.is_a?(Nokogiri::XML::Node) }
|
76
|
+
.each { |child| lines << child.text.strip }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
lines
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|