Class: RSX::Transformer
- Inherits:
-
Object
- Object
- RSX::Transformer
- Defined in:
- lib/rsx/transformer.rb
Overview
Transforms .rsx source (Ruby with embedded JSX-style markup) into plain Ruby.
The transformer is a single-pass scanner. Ruby is copied through verbatim while enough lexical state is tracked to know two things:
1. whether a "<" starts markup or is an operator, which comes down to
whether an expression is expected at that position (exactly how Babel
decides that a "<" opens JSX), and
2. where the `end` matching a `component` block is, so the block can be
rewritten into a class definition.
Generated Ruby keeps the same line numbering as the source, so exceptions raised inside a template point at the .rsx file and line the user wrote.
Constant Summary collapse
- IDENT_START =
/[A-Za-z_]/- IDENT_CHAR =
/[A-Za-z0-9_]/- TAG_START =
/[A-Za-z_>]/- TAG_CHAR =
/[A-Za-z0-9_\-.:]/- ATTR_START =
/[A-Za-z_@]/- ATTR_CHAR =
/[A-Za-z0-9_\-.:]/- DIGIT =
/[0-9]/- OPENS_EXPRESSION =
Keywords after which an expression is expected.
%w[ and begin break case do elsif else ensure if in next not or raise rescue return then unless until when while yield ].to_h { |word| [word, true] }.freeze
- CLOSES_EXPRESSION =
Keywords that terminate an expression.
%w[ end self nil true false __FILE__ __LINE__ __dir__ __method__ ].to_h { |word| [word, true] }.freeze
- BLOCK_KEYWORDS =
Keywords that open a block terminated by
end. %w[begin case class def module].to_h { |word| [word, true] }.freeze
- MODIFIER_KEYWORDS =
Block keywords that are also statement modifiers (
x if y). %w[if unless while until].to_h { |word| [word, true] }.freeze
- COMPONENT_MARKER =
Appended to compiled output when the file declares components, so a file of components can be told apart from a file of markup without running it.
"# rsx:components"
Class Method Summary collapse
- .defines_components?(ruby) ⇒ Boolean
-
.normalize_text(raw) ⇒ Object
JSX whitespace rules: indentation-only lines disappear, and remaining lines are joined with a single space.
- .transform(source, path: nil) ⇒ Object
Instance Method Summary collapse
-
#initialize(source, path: nil) ⇒ Transformer
constructor
A new instance of Transformer.
- #transform ⇒ Object
Constructor Details
#initialize(source, path: nil) ⇒ Transformer
Returns a new instance of Transformer.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/rsx/transformer.rb', line 51 def initialize(source, path: nil) @src = source.to_s @path = path @len = @src.length @pos = 0 @line = 1 @out_line = 1 @prev = :start @buffers = [+""] @heredocs = [] @blocks = [] @pending_loop = false @jsx_spans = [] @components = 0 # Static markup slots are keyed by a digest of the source, so recompiling # the same file reuses the same slots instead of leaking new ones. @codegen = Codegen.new(path: path, prefix: "#{Digest::SHA256.hexdigest(@src)[0, 10]}-") end |
Class Method Details
.defines_components?(ruby) ⇒ Boolean
88 89 90 |
# File 'lib/rsx/transformer.rb', line 88 def self.defines_components?(ruby) ruby.include?(COMPONENT_MARKER) end |
.normalize_text(raw) ⇒ Object
JSX whitespace rules: indentation-only lines disappear, and remaining lines are joined with a single space.
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 |
# File 'lib/rsx/transformer.rb', line 1006 def self.normalize_text(raw) lines = raw.split("\n", -1) return lines.first.to_s if lines.length == 1 kept = [] lines.each_with_index do |line, index| text = line text = text.sub(/\A[ \t\r]+/, "") if index.positive? text = text.sub(/[ \t\r]+\z/, "") if index < lines.length - 1 kept << text unless text.empty? end kept.join(" ") end |
.transform(source, path: nil) ⇒ Object
47 48 49 |
# File 'lib/rsx/transformer.rb', line 47 def self.transform(source, path: nil) new(source, path: path).transform end |
Instance Method Details
#transform ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/rsx/transformer.rb', line 70 def transform scan(:toplevel) unless @blocks.empty? component = @blocks.reverse.find { |frame| frame[:kind] == :component } if component raise SyntaxError.new("unterminated `component #{component[:name]}` block", path: @path, line: component[:line]) end end # The marker goes after the last line, so line numbers are unaffected. if @components.positive? write("\n") unless @buffers.first.empty? || @buffers.first.end_with?("\n") write("#{COMPONENT_MARKER}\n") end @buffers.first end |