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.
@@ -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