phlexing 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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 = Phlex::HTML::VOID_ELEMENTS.values + Phlex::HTML::STANDARD_ELEMENTS.values
8
+ KNOWN_ELEMENTS =
9
+ Phlex::HTML::VoidElements.registered_elements.values +
10
+ Phlex::HTML::StandardElements.registered_elements.values
8
11
 
9
- def indent(level)
10
- return "" if level == 1
11
-
12
- " " * level
12
+ def whitespace
13
+ options.whitespace? ? "whitespace\n" : ""
13
14
  end
14
15
 
15
- def whitespace(options)
16
- options.fetch(:whitespace, true) ? "whitespace\n" : ""
16
+ def newline
17
+ "\n"
17
18
  end
18
19
 
19
- def double_quote(string)
20
- "\"#{string}\""
20
+ def symbol(string)
21
+ ":#{string}"
21
22
  end
22
23
 
23
- def single_quote(string)
24
- "'#{string}'"
24
+ def arg(string)
25
+ "#{string}: "
25
26
  end
26
27
 
27
- def percent_literal_string(string)
28
+ def quote(string)
28
29
  "%(#{string})"
29
30
  end
30
31
 
31
- def quote(string)
32
- return double_quote(string) unless string.include?('"')
33
- return single_quote(string) unless string.include?("'")
32
+ def parens(string)
33
+ "(#{string})"
34
+ end
35
+
36
+ def braces(string)
37
+ "{ #{string} }"
38
+ end
34
39
 
35
- percent_literal_string(string)
40
+ def interpolate(string)
41
+ "\#\{#{string}\}"
36
42
  end
37
43
 
38
- def node_name(node)
39
- return "template_tag" if node.name == "template"
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
- name = node.name.gsub("-", "_")
61
+ def tag_name(node)
62
+ return "template_tag" if node.name == "template-tag"
42
63
 
43
- @custom_elements << name unless KNOWN_ELEMENTS.include?(name)
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 do_block_start
49
- " do\n"
71
+ def block
72
+ out << " {"
73
+ yield
74
+ out << " }"
50
75
  end
51
76
 
52
- def do_block_end(level = 0)
53
- "#{indent(level)}end\n"
77
+ def output(name, string)
78
+ out << name
79
+ out << " "
80
+ out << string.strip
81
+ out << newline
54
82
  end
55
83
 
56
- def multi_line_block(level)
57
- @buffer << " do\n"
58
- yield
59
- @buffer << ("#{indent(level)}end\n")
84
+ def blocklist
85
+ [
86
+ "render"
87
+ ]
60
88
  end
61
89
 
62
- def single_line_block
63
- @buffer << " { "
64
- yield
65
- @buffer << " }\n"
90
+ def routes_helpers
91
+ [
92
+ /\w+_url/,
93
+ /\w+_path/
94
+ ]
66
95
  end
67
96
 
68
- def erb_node?(node)
69
- node.is_a?(Nokogiri::XML::Element) && node.name == "erb"
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 element_node?(node)
73
- node.is_a?(Nokogiri::XML::Element)
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 text_node?(node)
77
- node.is_a?(Nokogiri::XML::Text)
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;", "&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
@@ -2,15 +2,18 @@
2
2
 
3
3
  module Phlexing
4
4
  class NameSuggestor
5
- def self.suggest(html)
6
- converter = Phlexing::Converter.new(html)
5
+ using Refinements::StringRefinements
7
6
 
8
- ivars = converter.ivars
9
- locals = converter.locals
7
+ def self.call(source)
8
+ document = Parser.call(source)
9
+ analyzer = RubyAnalyzer.call(source)
10
10
 
11
- ids = extract(converter, :extract_id_from_element)
12
- classes = extract(converter, :extract_class_from_element)
13
- tags = extract(converter, :extract_tag_name_from_element)
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".gsub("-", "_").gsub(" ", "_").camelize
30
+ "#{name}_component".underscore.camelize
28
31
  end
29
32
 
30
- def self.extract(converter, method)
31
- return [] unless converter.parsed
33
+ def self.extract(document, method)
34
+ return [] unless document
32
35
 
33
- converter.parsed.children.map { |element| send(method, element) }.compact
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.try(:[], "id")
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.try(:[], "class")
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="&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