Class: RSX::Transformer

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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

Returns:

  • (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

#transformObject



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