phlexing 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def indent(level)
6
- return "" if level == 1
8
+ KNOWN_ELEMENTS =
9
+ Phlex::HTML::VoidElements.registered_elements.values +
10
+ Phlex::HTML::StandardElements.registered_elements.values
7
11
 
8
- " " * level
12
+ def whitespace
13
+ options.whitespace? ? "whitespace\n" : ""
9
14
  end
10
15
 
11
- def whitespace(options)
12
- options.fetch(:whitespace, true) ? "whitespace\n" : ""
16
+ def newline
17
+ "\n"
13
18
  end
14
19
 
15
- def double_quote(string)
16
- "\"#{string}\""
20
+ def symbol(string)
21
+ ":#{string}"
17
22
  end
18
23
 
19
- def single_quote(string)
20
- "'#{string}'"
24
+ def arg(string)
25
+ "#{string}: "
21
26
  end
22
27
 
23
- def percent_literal_string(string)
28
+ def quote(string)
24
29
  "%(#{string})"
25
30
  end
26
31
 
27
- def quote(string)
28
- return double_quote(string) unless string.include?('"')
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 node_name(node)
35
- return "template_tag" if node.name == "template"
36
- return node.name unless node.name.include?("-")
37
-
38
- name = node.name.gsub("-", "_")
39
- @custom_elements << name
36
+ def braces(string)
37
+ "{ #{string} }"
38
+ end
40
39
 
41
- name
40
+ def interpolate(string)
41
+ "\#\{#{string}\}"
42
42
  end
43
43
 
44
- def do_block_start
45
- " do\n"
44
+ def unescape(source)
45
+ CGI.unescapeHTML(source)
46
46
  end
47
47
 
48
- def do_block_end(level = 0)
49
- "#{indent(level)}end\n"
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 multi_line_block(level)
53
- @buffer << " do\n"
54
- yield
55
- @buffer << ("#{indent(level)}end\n")
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 single_line_block
59
- @buffer << " { "
71
+ def block
72
+ out << " {"
60
73
  yield
61
- @buffer << " }\n"
74
+ out << " }"
62
75
  end
63
76
 
64
- def erb_node?(node)
65
- node.is_a?(Nokogiri::XML::Element) && node.name == "erb"
77
+ def output(name, string)
78
+ out << name
79
+ out << " "
80
+ out << string.strip
81
+ out << newline
66
82
  end
67
83
 
68
- def element_node?(node)
69
- node.is_a?(Nokogiri::XML::Element)
84
+ def blocklist
85
+ [
86
+ "render"
87
+ ]
70
88
  end
71
89
 
72
- def text_node?(node)
73
- node.is_a?(Nokogiri::XML::Text)
90
+ def routes_helpers
91
+ [
92
+ /\w+_url/,
93
+ /\w+_path/
94
+ ]
74
95
  end
75
96
 
76
- def multiple_children?(node)
77
- node.children.length > 1
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 siblings?(node)
81
- multiple_children?(node.parent)
82
- end
109
+ def string_output?(node)
110
+ word = node.text.strip.scan(/^\w+/)[0]
111
+
112
+ return true if word.nil?
83
113
 
84
- def erb_interpolation?(node)
85
- first = node.children.first
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
- erb_node?(node) &&
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 erb_safe_output?(node)
95
- erb_interpolation?(node) && node.text.start_with?("=")
120
+ def children?(node)
121
+ node.children.length >= 1
96
122
  end
97
123
 
98
- def erb_comment?(node)
99
- node.attributes["comment"]
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;", "&lt;")
33
+ .gsub("& quot;", "&quot;")
34
+ .gsub("& gt;", "&gt;")
35
+ .gsub("& #amp;", "&#amp;")
36
+ .gsub("& #38;", "&#38;")
37
+ .gsub("& #60;", "&#60;")
38
+ .gsub("& #62;", "&#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="&lt;%= something? ? &quot;class-1&quot; : &quot;class-2&quot; %&gt;">Text</div>
17
+ #
18
+ # into this output:
19
+ # <div data-erb-class="&lt;%= 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