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