ruby_ui_converter 0.1.0 → 0.2.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.
@@ -31,10 +31,10 @@ module RubyUIConverter
31
31
  end
32
32
 
33
33
  def meaningful(nodes)
34
- nodes.reject do |node|
35
- (node.is_a?(Nodes::Text) && node.content.strip.empty?) ||
34
+ nodes.each_with_index.reject do |node, index|
35
+ (node.is_a?(Nodes::Text) && node.content.strip.empty? && !inline_space?(nodes, index)) ||
36
36
  (node.is_a?(Nodes::RawText) && node.content.strip.empty?)
37
- end
37
+ end.map(&:first)
38
38
  end
39
39
 
40
40
  def render_attrs(attributes, except: [])
@@ -156,6 +156,7 @@ module RubyUIConverter
156
156
  # Collapse runs of whitespace to a single space but keep meaningful
157
157
  # leading/trailing spaces so inline text ("Hello, ") stays readable.
158
158
  content = node.content.gsub(/\s+/, " ")
159
+ return builder.line("whitespace") if content == " " # see #inline_space?
159
160
  return if content.strip.empty?
160
161
 
161
162
  builder.line("plain #{ruby_string(content)}")
@@ -228,14 +229,22 @@ module RubyUIConverter
228
229
  case node
229
230
  when Nodes::Output, Nodes::Statement then acc << node.code
230
231
  when Nodes::Control then node.branches.each { |b| collect_codes(b.children, acc) }
231
- when Nodes::Element then collect_codes(node.children, acc)
232
+ when Nodes::Element, Nodes::Comment then collect_codes(node.children, acc)
232
233
  end
233
234
  end
234
235
  acc
235
236
  end
236
237
 
237
238
  def emit_comment(node, builder)
238
- if node.html
239
+ if node.html && meaningful(node.children).any?
240
+ builder.line("comment do")
241
+ builder.indent
242
+ text = node.text.strip
243
+ builder.line("plain #{ruby_string(text)}") unless text.empty?
244
+ emit_children(meaningful(node.children), builder)
245
+ builder.dedent
246
+ builder.line("end")
247
+ elsif node.html
239
248
  text = node.text.strip
240
249
  builder.line("comment { #{ruby_string(text)} }") unless text.empty?
241
250
  else
@@ -276,8 +285,43 @@ module RubyUIConverter
276
285
 
277
286
  # Expressions with whitespace (`dom_id user`, `a ? b : c`) would parse
278
287
  # incorrectly inside the attribute argument list, so wrap them in parens.
288
+ # Self-delimited expressions (`[a, b]`, `{a: 1}`, `dom_id(user, :x)`) are
289
+ # safe as-is.
279
290
  def bare_expression(code)
280
- code =~ /\s/ ? "(#{code})" : code
291
+ return code unless code =~ /\s/
292
+
293
+ self_delimited?(code) ? code : "(#{code})"
294
+ end
295
+
296
+ GROUP_TOKENS = {
297
+ on_lparen: 1, on_lbracket: 1, on_lbrace: 1,
298
+ on_rparen: -1, on_rbracket: -1, on_rbrace: -1
299
+ }.freeze
300
+ CALL_PREFIX_TOKENS = %i[on_ident on_const on_ivar on_cvar on_gvar on_period].freeze
301
+
302
+ # True when the code is a bracketed literal or a parenthesized call
303
+ # (optionally on a receiver chain written without spaces) whose opening
304
+ # bracket closes exactly at the end of the code.
305
+ def self_delimited?(code)
306
+ tokens = Ripper.lex(code)
307
+ index = 0
308
+ while index < tokens.length &&
309
+ (CALL_PREFIX_TOKENS.include?(tokens[index][1]) ||
310
+ (tokens[index][1] == :on_op && ["::", "&."].include?(tokens[index][2])))
311
+ index += 1
312
+ end
313
+ return false if index.positive? && !%i[on_ident on_const].include?(tokens[index - 1][1])
314
+ return false unless tokens[index] && GROUP_TOKENS[tokens[index][1]] == 1
315
+ return false if index.positive? && tokens[index][1] != :on_lparen
316
+
317
+ depth = 0
318
+ tokens[index..].each_with_index do |token, offset|
319
+ depth += GROUP_TOKENS.fetch(token[1], 0)
320
+ return index + offset == tokens.length - 1 if depth.zero?
321
+ end
322
+ false
323
+ rescue StandardError
324
+ false
281
325
  end
282
326
 
283
327
  def interpolated(parts)
@@ -299,6 +343,15 @@ module RubyUIConverter
299
343
  name.to_s.downcase
300
344
  end
301
345
 
346
+ # A whitespace-only text node on a single line between two siblings
347
+ # (`<em>a</em> <%= b %>`) separates inline content and is kept (emitted as
348
+ # Phlex's `whitespace`); whitespace spanning lines between tags is layout
349
+ # and is dropped.
350
+ def inline_space?(nodes, index)
351
+ content = nodes[index].content
352
+ !content.empty? && !content.include?("\n") && index.positive? && index < nodes.length - 1
353
+ end
354
+
302
355
  def inlineable?(node)
303
356
  node.is_a?(Nodes::Text) ||
304
357
  (node.is_a?(Nodes::Output) && !node.raw && !special_output?(node.code))
@@ -385,16 +438,24 @@ module RubyUIConverter
385
438
  "\"#{escape_inner(string)}\""
386
439
  end
387
440
 
441
+ STRING_ESCAPES = {
442
+ "\\" => "\\\\",
443
+ "\"" => "\\\"",
444
+ "\n" => "\\n",
445
+ "\t" => "\\t",
446
+ "\r" => "\\r",
447
+ "\#{" => "\\\#{"
448
+ }.freeze
449
+
450
+ # Invisible / easily-confused characters (no-break and other Unicode
451
+ # spaces, soft hyphen, zero-width characters, BOM) — typically from a
452
+ # decoded `&nbsp;` — are written as \u escapes so the generated source
453
+ # stays readable.
454
+ INVISIBLE_CHARS = /[ ­ -‏
-  -⁤]/
455
+
388
456
  def escape_inner(string)
389
- string.to_s.gsub(/[\\"\n\t\r]|\#\{/) do |match|
390
- {
391
- "\\" => "\\\\",
392
- "\"" => "\\\"",
393
- "\n" => "\\n",
394
- "\t" => "\\t",
395
- "\r" => "\\r",
396
- "\#{" => "\\\#{"
397
- }[match]
457
+ string.to_s.gsub(/[\\"\n\t\r]|\#\{|#{INVISIBLE_CHARS}/o) do |match|
458
+ STRING_ESCAPES.fetch(match) { format("\\u%04X", match.ord) }
398
459
  end
399
460
  end
400
461
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyUIConverter
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -5,9 +5,13 @@ require "strscan"
5
5
 
6
6
  require_relative "ruby_ui_converter/version"
7
7
  require_relative "ruby_ui_converter/nodes"
8
+ require_relative "ruby_ui_converter/html_entities"
8
9
  require_relative "ruby_ui_converter/lexer"
9
10
  require_relative "ruby_ui_converter/html_tokenizer"
10
11
  require_relative "ruby_ui_converter/parser"
12
+ require_relative "ruby_ui_converter/ruby_literal"
13
+ require_relative "ruby_ui_converter/haml_attributes"
14
+ require_relative "ruby_ui_converter/haml_parser"
11
15
  require_relative "ruby_ui_converter/code_builder"
12
16
  require_relative "ruby_ui_converter/naming"
13
17
  require_relative "ruby_ui_converter/rails_helpers"
@@ -24,13 +28,35 @@ require_relative "ruby_ui_converter/doctor"
24
28
  module RubyUIConverter
25
29
  class Error < StandardError; end
26
30
 
27
- # Convert a single .erb string into Ruby/Phlex source (no file IO).
31
+ # Template file extension -> source format (front-end parser).
32
+ FORMATS = { ".erb" => :erb, ".haml" => :haml }.freeze
33
+
34
+ # The source format of a template path ("index.html.haml" -> :haml), or
35
+ # nil when it isn't a supported template.
36
+ def self.format_for(path)
37
+ FORMATS[File.extname(path.to_s)]
38
+ end
39
+
40
+ # Parse template source into the shared AST (Nodes::Document). This is the
41
+ # single dispatch point between the front-ends: ERB (Lexer + HtmlTokenizer
42
+ # + Parser) and HAML (HamlParser, backed by the optional haml gem).
43
+ def self.parse(source, format: :erb)
44
+ case format.to_sym
45
+ when :erb then Parser.new(source).parse
46
+ when :haml then HamlParser.new(source).parse
47
+ else raise Error, "unsupported template format: #{format.inspect}"
48
+ end
49
+ end
50
+
51
+ # Convert a single .erb (or, with format: :haml, .haml) string into
52
+ # Ruby/Phlex source (no file IO).
28
53
  #
29
54
  # RubyUIConverter.convert_string("<h1><%= @title %></h1>")
55
+ # RubyUIConverter.convert_string("%h1= @title", format: :haml)
30
56
  def self.convert_string(source, class_name: "Component", base_namespace: "",
31
- base_class: "Phlex::HTML", **opts)
57
+ base_class: "Phlex::HTML", format: :erb, **opts)
32
58
  config = Configuration.new(base_namespace: base_namespace, base_class: base_class, **opts)
33
- document = Parser.new(source).parse
59
+ document = parse(source, format: format)
34
60
  builder = CodeBuilder.new(indent: config.indent)
35
61
  builder.line("class #{class_name} < #{config.base_class}")
36
62
  builder.indent
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_ui_converter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jackson Pires
@@ -51,11 +51,26 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '5.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: haml
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '5.2'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '5.2'
54
68
  description: |
55
69
  ruby_ui_converter walks a Rails views directory recursively and converts each
56
- .erb template into an equivalent .rb file written with Phlex (and RubyUI when
57
- configured). Traditional Rails partials (_partial.html.erb) are converted into
58
- their own Phlex component classes.
70
+ .erb or .haml template into an equivalent .rb file written with Phlex (and
71
+ RubyUI when configured). Traditional Rails partials (_partial.html.erb) are
72
+ converted into their own Phlex component classes. HAML conversion uses the
73
+ haml gem when it is available (optional dependency).
59
74
  email:
60
75
  - jackson@linkana.com
61
76
  executables:
@@ -76,6 +91,9 @@ files:
76
91
  - lib/ruby_ui_converter/doctor.rb
77
92
  - lib/ruby_ui_converter/file_walker.rb
78
93
  - lib/ruby_ui_converter/form_builder.rb
94
+ - lib/ruby_ui_converter/haml_attributes.rb
95
+ - lib/ruby_ui_converter/haml_parser.rb
96
+ - lib/ruby_ui_converter/html_entities.rb
79
97
  - lib/ruby_ui_converter/html_tokenizer.rb
80
98
  - lib/ruby_ui_converter/lexer.rb
81
99
  - lib/ruby_ui_converter/locals_detector.rb
@@ -83,6 +101,7 @@ files:
83
101
  - lib/ruby_ui_converter/nodes.rb
84
102
  - lib/ruby_ui_converter/parser.rb
85
103
  - lib/ruby_ui_converter/rails_helpers.rb
104
+ - lib/ruby_ui_converter/ruby_literal.rb
86
105
  - lib/ruby_ui_converter/template.rb
87
106
  - lib/ruby_ui_converter/transformer.rb
88
107
  - lib/ruby_ui_converter/version.rb
@@ -108,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
127
  - !ruby/object:Gem::Version
109
128
  version: '0'
110
129
  requirements: []
111
- rubygems_version: 4.0.10
130
+ rubygems_version: 4.0.3
112
131
  specification_version: 4
113
- summary: Convert Rails .erb views and partials into RubyUI/Phlex Ruby components.
132
+ summary: Convert Rails .erb and .haml views and partials into RubyUI/Phlex Ruby components.
114
133
  test_files: []