phlexing 0.3.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 -230
- data/lib/phlexing/erb_transformer.rb +46 -0
- data/lib/phlexing/formatter.rb +22 -0
- data/lib/phlexing/helpers.rb +82 -56
- data/lib/phlexing/minifier.rb +41 -0
- data/lib/phlexing/name_suggestor.rb +16 -13
- 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 +87 -5
- data/lib/phlexing.rb +12 -1
- metadata +35 -12
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "syntax_tree"
|
4
|
+
|
5
|
+
module Phlexing
|
6
|
+
class Formatter
|
7
|
+
def self.call(...)
|
8
|
+
new(...).call
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(source, max: 80)
|
12
|
+
@source = source.to_s.dup
|
13
|
+
@max = max
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
SyntaxTree.format(@source, @max).strip
|
18
|
+
rescue SyntaxTree::Parser::ParseError, NoMethodError
|
19
|
+
@source
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/phlexing/helpers.rb
CHANGED
@@ -1,80 +1,124 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "phlex"
|
4
|
+
require "phlex-rails"
|
4
5
|
|
5
6
|
module Phlexing
|
6
7
|
module Helpers
|
7
|
-
KNOWN_ELEMENTS =
|
8
|
+
KNOWN_ELEMENTS =
|
9
|
+
Phlex::HTML::VoidElements.registered_elements.values +
|
10
|
+
Phlex::HTML::StandardElements.registered_elements.values
|
8
11
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
" " * level
|
12
|
+
def whitespace
|
13
|
+
options.whitespace? ? "whitespace\n" : ""
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
|
16
|
+
def newline
|
17
|
+
"\n"
|
17
18
|
end
|
18
19
|
|
19
|
-
def
|
20
|
-
"
|
20
|
+
def symbol(string)
|
21
|
+
":#{string}"
|
21
22
|
end
|
22
23
|
|
23
|
-
def
|
24
|
-
"
|
24
|
+
def arg(string)
|
25
|
+
"#{string}: "
|
25
26
|
end
|
26
27
|
|
27
|
-
def
|
28
|
+
def quote(string)
|
28
29
|
"%(#{string})"
|
29
30
|
end
|
30
31
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
32
|
+
def parens(string)
|
33
|
+
"(#{string})"
|
34
|
+
end
|
35
|
+
|
36
|
+
def braces(string)
|
37
|
+
"{ #{string} }"
|
38
|
+
end
|
34
39
|
|
35
|
-
|
40
|
+
def interpolate(string)
|
41
|
+
"\#\{#{string}\}"
|
36
42
|
end
|
37
43
|
|
38
|
-
def
|
39
|
-
|
44
|
+
def unescape(source)
|
45
|
+
CGI.unescapeHTML(source)
|
46
|
+
end
|
47
|
+
|
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
|
59
|
+
end
|
40
60
|
|
41
|
-
|
61
|
+
def tag_name(node)
|
62
|
+
return "template_tag" if node.name == "template-tag"
|
42
63
|
|
43
|
-
|
64
|
+
name = node.name.tr("-", "_")
|
65
|
+
|
66
|
+
@converter.custom_elements << name unless KNOWN_ELEMENTS.include?(name)
|
44
67
|
|
45
68
|
name
|
46
69
|
end
|
47
70
|
|
48
|
-
def
|
49
|
-
"
|
71
|
+
def block
|
72
|
+
out << " {"
|
73
|
+
yield
|
74
|
+
out << " }"
|
50
75
|
end
|
51
76
|
|
52
|
-
def
|
53
|
-
|
77
|
+
def output(name, string)
|
78
|
+
out << name
|
79
|
+
out << " "
|
80
|
+
out << string.strip
|
81
|
+
out << newline
|
54
82
|
end
|
55
83
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
84
|
+
def blocklist
|
85
|
+
[
|
86
|
+
"render"
|
87
|
+
]
|
60
88
|
end
|
61
89
|
|
62
|
-
def
|
63
|
-
|
64
|
-
|
65
|
-
|
90
|
+
def routes_helpers
|
91
|
+
[
|
92
|
+
/\w+_url/,
|
93
|
+
/\w+_path/
|
94
|
+
]
|
66
95
|
end
|
67
96
|
|
68
|
-
def
|
69
|
-
|
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
|
+
}
|
70
107
|
end
|
71
108
|
|
72
|
-
def
|
73
|
-
node.
|
109
|
+
def string_output?(node)
|
110
|
+
word = node.text.strip.scan(/^\w+/)[0]
|
111
|
+
|
112
|
+
return true if word.nil?
|
113
|
+
|
114
|
+
blocklist_matched = known_rails_helpers.keys.include?(word) || blocklist.include?(word)
|
115
|
+
route_matched = routes_helpers.map { |regex| word.scan(regex).any? }.reduce(:|)
|
116
|
+
|
117
|
+
!(blocklist_matched || route_matched)
|
74
118
|
end
|
75
119
|
|
76
|
-
def
|
77
|
-
node.
|
120
|
+
def children?(node)
|
121
|
+
node.children.length >= 1
|
78
122
|
end
|
79
123
|
|
80
124
|
def multiple_children?(node)
|
@@ -84,23 +128,5 @@ module Phlexing
|
|
84
128
|
def siblings?(node)
|
85
129
|
multiple_children?(node.parent)
|
86
130
|
end
|
87
|
-
|
88
|
-
def erb_interpolation?(node)
|
89
|
-
first = node.children.first
|
90
|
-
|
91
|
-
erb_node?(node) &&
|
92
|
-
node.children.one? &&
|
93
|
-
first.children.none? &&
|
94
|
-
text_node?(first) &&
|
95
|
-
node.attributes["interpolated"]
|
96
|
-
end
|
97
|
-
|
98
|
-
def erb_safe_output?(node)
|
99
|
-
erb_interpolation?(node) && node.text.start_with?("=")
|
100
|
-
end
|
101
|
-
|
102
|
-
def erb_comment?(node)
|
103
|
-
node.attributes["comment"]
|
104
|
-
end
|
105
131
|
end
|
106
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
|
@@ -2,15 +2,18 @@
|
|
2
2
|
|
3
3
|
module Phlexing
|
4
4
|
class NameSuggestor
|
5
|
-
|
6
|
-
converter = Phlexing::Converter.new(html)
|
5
|
+
using Refinements::StringRefinements
|
7
6
|
|
8
|
-
|
9
|
-
|
7
|
+
def self.call(source)
|
8
|
+
document = Parser.call(source)
|
9
|
+
analyzer = RubyAnalyzer.call(source)
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
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)
|
14
17
|
|
15
18
|
return wrap(ivars.first) if ivars.one? && locals.none?
|
16
19
|
return wrap(locals.first) if locals.one? && ivars.none?
|
@@ -24,20 +27,20 @@ module Phlexing
|
|
24
27
|
end
|
25
28
|
|
26
29
|
def self.wrap(name)
|
27
|
-
"#{name}_component".
|
30
|
+
"#{name}_component".underscore.camelize
|
28
31
|
end
|
29
32
|
|
30
|
-
def self.extract(
|
31
|
-
return [] unless
|
33
|
+
def self.extract(document, method)
|
34
|
+
return [] unless document
|
32
35
|
|
33
|
-
|
36
|
+
document.children.map { |element| send(method, element) }.compact
|
34
37
|
end
|
35
38
|
|
36
39
|
def self.extract_id_from_element(element)
|
37
40
|
return if element.nil?
|
38
41
|
return if element.is_a?(Nokogiri::XML::Text)
|
39
42
|
|
40
|
-
id_attribute = element.attributes.
|
43
|
+
id_attribute = element.attributes && element.attributes["id"]
|
41
44
|
return if id_attribute.nil?
|
42
45
|
|
43
46
|
id = id_attribute.value.to_s.strip
|
@@ -50,7 +53,7 @@ module Phlexing
|
|
50
53
|
return if element.nil?
|
51
54
|
return if element.is_a?(Nokogiri::XML::Text)
|
52
55
|
|
53
|
-
class_attribute = element.attributes.
|
56
|
+
class_attribute = element.attributes && element.attributes["class"]
|
54
57
|
|
55
58
|
return if class_attribute.nil?
|
56
59
|
|
@@ -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
|