rsx-rb 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +1022 -0
- data/examples/components/button.rsx +22 -0
- data/examples/components/card.rsx +21 -0
- data/examples/components/sidebar.rsx +29 -0
- data/examples/components/theme.rsx +29 -0
- data/examples/components/user_table.rsx +64 -0
- data/examples/user_profile.rsx +51 -0
- data/examples/views/dashboard.html.rsx +26 -0
- data/exe/rsx +10 -0
- data/lib/rsx/attributes.rb +313 -0
- data/lib/rsx/cache.rb +102 -0
- data/lib/rsx/children.rb +78 -0
- data/lib/rsx/cli.rb +96 -0
- data/lib/rsx/codegen.rb +320 -0
- data/lib/rsx/compile_cache.rb +84 -0
- data/lib/rsx/component.rb +212 -0
- data/lib/rsx/context.rb +67 -0
- data/lib/rsx/errors.rb +27 -0
- data/lib/rsx/escape.rb +59 -0
- data/lib/rsx/helpers.rb +29 -0
- data/lib/rsx/loader.rb +194 -0
- data/lib/rsx/nodes.rb +26 -0
- data/lib/rsx/railtie.rb +81 -0
- data/lib/rsx/safe_string.rb +42 -0
- data/lib/rsx/tasks.rake +26 -0
- data/lib/rsx/template.rb +63 -0
- data/lib/rsx/template_handler.rb +35 -0
- data/lib/rsx/transformer.rb +1034 -0
- data/lib/rsx/version.rb +8 -0
- data/lib/rsx-rb.rb +4 -0
- data/lib/rsx.rb +430 -0
- metadata +86 -0
data/lib/rsx/children.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSX
|
|
4
|
+
# The `children` prop of a component element.
|
|
5
|
+
#
|
|
6
|
+
# Children compile to a block rather than a finished string, so they are
|
|
7
|
+
# rendered *inside* the component that received them. That is what lets
|
|
8
|
+
# <Theme.Provider> change what its children see, lets a caching component skip
|
|
9
|
+
# building them at all, and means a component that never renders {children}
|
|
10
|
+
# never pays for them.
|
|
11
|
+
#
|
|
12
|
+
# The block's value is memoized, and kept in its raw form as well, so React's
|
|
13
|
+
# render-prop pattern works: <Consumer>{->(value) { ... }}</Consumer>.
|
|
14
|
+
class Children
|
|
15
|
+
UNSET = Object.new
|
|
16
|
+
private_constant :UNSET
|
|
17
|
+
|
|
18
|
+
def initialize(&block)
|
|
19
|
+
@block = block
|
|
20
|
+
@value = UNSET
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The value the children expression evaluated to, before coercion.
|
|
24
|
+
def value
|
|
25
|
+
@value = @block.call if @value.equal?(UNSET)
|
|
26
|
+
@value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The rendered markup. Used whenever children are interpolated as {children}.
|
|
30
|
+
def render
|
|
31
|
+
@render ||= RSX.child(value)
|
|
32
|
+
end
|
|
33
|
+
alias to_rsx render
|
|
34
|
+
alias to_safe_string render
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
render.to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_str
|
|
41
|
+
render.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def html_safe?
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Render props: {children.call(item)} passes a value back to the caller.
|
|
49
|
+
def call(*arguments, **options, &block)
|
|
50
|
+
raw = value
|
|
51
|
+
return raw.call(*arguments, **options, &block) if raw.respond_to?(:call)
|
|
52
|
+
|
|
53
|
+
raw
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def empty?
|
|
57
|
+
render.empty?
|
|
58
|
+
end
|
|
59
|
+
alias blank? empty?
|
|
60
|
+
|
|
61
|
+
def present?
|
|
62
|
+
!empty?
|
|
63
|
+
end
|
|
64
|
+
alias any? present?
|
|
65
|
+
|
|
66
|
+
def length
|
|
67
|
+
render.length
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ==(other)
|
|
71
|
+
to_s == other.to_s
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def inspect
|
|
75
|
+
"#<RSX::Children #{@render ? @render.inspect : "(not rendered)"}>"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/rsx/cli.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module RSX
|
|
6
|
+
# `rsx` command line tool. Useful for seeing exactly what a template compiles
|
|
7
|
+
# to, rendering a file outside of Rails, and warming the compile cache.
|
|
8
|
+
class CLI
|
|
9
|
+
BANNER = <<~TEXT
|
|
10
|
+
Usage: rsx COMMAND [options] [files]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
compile FILE... Print the Ruby a .rsx file compiles to
|
|
14
|
+
render FILE Render a .rsx file and print the HTML
|
|
15
|
+
precompile DIR... Compile every .rsx file under DIR and cache the result
|
|
16
|
+
version Print the RSX version
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def self.start(argv)
|
|
22
|
+
new.run(argv)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(argv)
|
|
26
|
+
options = { props: {}, cache_dir: nil }
|
|
27
|
+
|
|
28
|
+
parser = OptionParser.new do |opts|
|
|
29
|
+
opts.banner = BANNER
|
|
30
|
+
opts.on("-p", "--prop NAME=VALUE", "Pass a string prop when rendering") do |pair|
|
|
31
|
+
name, value = pair.split("=", 2)
|
|
32
|
+
options[:props][name.to_sym] = value
|
|
33
|
+
end
|
|
34
|
+
opts.on("-I", "--include PATH", "Add a directory to the RSX load path") do |path|
|
|
35
|
+
RSX.config.paths |= [File.expand_path(path)]
|
|
36
|
+
end
|
|
37
|
+
opts.on("-c", "--cache-dir DIR", "Directory for compiled output") do |dir|
|
|
38
|
+
options[:cache_dir] = dir
|
|
39
|
+
end
|
|
40
|
+
opts.on("-h", "--help", "Show this message") { puts opts; return 0 }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
arguments = parser.parse(argv)
|
|
44
|
+
command = arguments.shift
|
|
45
|
+
|
|
46
|
+
RSX.config.cache_dir = options[:cache_dir]
|
|
47
|
+
|
|
48
|
+
case command
|
|
49
|
+
when "compile" then compile(arguments)
|
|
50
|
+
when "render" then render(arguments, options[:props])
|
|
51
|
+
when "precompile" then precompile(arguments, options[:cache_dir])
|
|
52
|
+
when "version", "-v", "--version" then puts RSX::VERSION
|
|
53
|
+
when nil then puts parser
|
|
54
|
+
else
|
|
55
|
+
warn "rsx: unknown command #{command.inspect}"
|
|
56
|
+
puts parser
|
|
57
|
+
return 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
0
|
|
61
|
+
rescue RSX::Error, Errno::ENOENT => e
|
|
62
|
+
warn "rsx: #{e.message}"
|
|
63
|
+
1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def compile(files)
|
|
69
|
+
abort_missing(files)
|
|
70
|
+
files.each do |file|
|
|
71
|
+
puts "# #{file}" if files.length > 1
|
|
72
|
+
puts RSX.compile(File.read(file), path: File.expand_path(file))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render(files, props)
|
|
77
|
+
abort_missing(files)
|
|
78
|
+
files.each { |file| puts RSX.render_file(File.expand_path(file), **props) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def precompile(directories, cache_dir)
|
|
82
|
+
directories = [Dir.pwd] if directories.empty?
|
|
83
|
+
RSX.config.cache_dir = cache_dir || File.join(Dir.pwd, "tmp", "cache", "rsx")
|
|
84
|
+
files = RSX.loader.files(directories)
|
|
85
|
+
RSX.precompile!(directories)
|
|
86
|
+
puts "rsx: precompiled #{files.length} file(s) into #{RSX.config.cache_dir}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def abort_missing(files)
|
|
90
|
+
raise Error, "no files given" if files.empty?
|
|
91
|
+
|
|
92
|
+
missing = files.reject { |file| File.file?(file) }
|
|
93
|
+
raise Error, "no such file: #{missing.join(", ")}" unless missing.empty?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/rsx/codegen.rb
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSX
|
|
4
|
+
# Turns an RSX node tree into Ruby source.
|
|
5
|
+
#
|
|
6
|
+
# Markup compiles to a single interpolated string literal: static text is baked
|
|
7
|
+
# in at compile time and only the {ruby} parts remain as interpolations. Nested
|
|
8
|
+
# elements are spliced into their parent's literal, so an entire static subtree
|
|
9
|
+
# collapses to one frozen string with no intermediate objects.
|
|
10
|
+
#
|
|
11
|
+
# <ul className="list"><li>{name}</li></ul>
|
|
12
|
+
#
|
|
13
|
+
# becomes
|
|
14
|
+
#
|
|
15
|
+
# ::RSX::SafeString.new("<ul class=\"list\"><li>#{::RSX.child(name)}</li></ul>")
|
|
16
|
+
class Codegen
|
|
17
|
+
LITERAL_ESCAPES = {
|
|
18
|
+
"\\" => "\\\\",
|
|
19
|
+
'"' => '\"',
|
|
20
|
+
"\n" => '\n',
|
|
21
|
+
"\t" => '\t',
|
|
22
|
+
"\r" => '\r',
|
|
23
|
+
"\e" => '\e',
|
|
24
|
+
"\0" => '\0'
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
LITERAL_PATTERN = /[\\"\n\t\r\e\0]|#(?=[{$@])/
|
|
28
|
+
|
|
29
|
+
INNER_HTML = %w[dangerouslySetInnerHTML dangerously_set_inner_html].freeze
|
|
30
|
+
|
|
31
|
+
def initialize(path: nil, prefix: nil)
|
|
32
|
+
@path = path
|
|
33
|
+
@prefix = prefix || "rsx"
|
|
34
|
+
@statics = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns [ruby_source, static?]
|
|
38
|
+
def compile(node, start_line:, end_line:)
|
|
39
|
+
parts = []
|
|
40
|
+
emit(node, parts)
|
|
41
|
+
parts = merge(parts)
|
|
42
|
+
static = parts.all? { |part| part[0] == :static }
|
|
43
|
+
[assemble(parts, start_line, end_line, static), static]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def emit(node, parts)
|
|
49
|
+
case node
|
|
50
|
+
when Nodes::Text
|
|
51
|
+
parts << [:static, Escape.static_text(node.value), node.line]
|
|
52
|
+
when Nodes::Expression
|
|
53
|
+
# The extra parentheses let a container hold anything Ruby accepts as an
|
|
54
|
+
# expression, including modifiers: {greeting if signed_in?}.
|
|
55
|
+
parts << [:dynamic, "::RSX.child((#{node.source}))", node.line]
|
|
56
|
+
when Nodes::Fragment
|
|
57
|
+
node.children.each { |child| emit(child, parts) }
|
|
58
|
+
when Nodes::Element
|
|
59
|
+
emit_element(node, parts)
|
|
60
|
+
when Nodes::Component
|
|
61
|
+
emit_component(node, parts)
|
|
62
|
+
else
|
|
63
|
+
raise Error, "unexpected node #{node.inspect}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# Elements
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def emit_element(node, parts)
|
|
72
|
+
tag = node.tag
|
|
73
|
+
parts << [:static, "<#{tag}", node.line]
|
|
74
|
+
|
|
75
|
+
inner_html = nil
|
|
76
|
+
attributes = node.attributes.reject do |attribute|
|
|
77
|
+
next false unless attribute.kind != :spread && INNER_HTML.include?(attribute.name)
|
|
78
|
+
|
|
79
|
+
inner_html = attribute
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if attributes.any? { |attribute| attribute.kind == :spread }
|
|
84
|
+
emit_spread_attributes(attributes, parts)
|
|
85
|
+
else
|
|
86
|
+
attributes.each { |attribute| emit_attribute(attribute, parts) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if inner_html
|
|
90
|
+
parts << [:static, ">", last_line(parts, node)]
|
|
91
|
+
parts << [:dynamic, "::RSX.raw_html(#{inner_html.value})", inner_html.line]
|
|
92
|
+
parts << [:static, "</#{tag}>", last_line(parts, node)]
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if Attributes.void?(tag)
|
|
97
|
+
parts << [:static, ">", last_line(parts, node)]
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if node.children.empty?
|
|
102
|
+
if node.self_closing && Attributes.self_closing?(tag)
|
|
103
|
+
parts << [:static, "/>", last_line(parts, node)]
|
|
104
|
+
else
|
|
105
|
+
parts << [:static, "></#{tag}>", last_line(parts, node)]
|
|
106
|
+
end
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
parts << [:static, ">", last_line(parts, node)]
|
|
111
|
+
node.children.each { |child| emit(child, parts) }
|
|
112
|
+
parts << [:static, "</#{tag}>", last_line(parts, node)]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# An element with a spread has all of its attributes merged at runtime, so a
|
|
116
|
+
# later value replaces an earlier one exactly as it would in React. Emitting
|
|
117
|
+
# them one by one would instead produce a duplicate HTML attribute, where
|
|
118
|
+
# the browser keeps the first.
|
|
119
|
+
def emit_spread_attributes(attributes, parts)
|
|
120
|
+
groups = []
|
|
121
|
+
attributes.each do |attribute|
|
|
122
|
+
if attribute.kind == :spread
|
|
123
|
+
groups << attribute.value
|
|
124
|
+
elsif groups.last.is_a?(Array)
|
|
125
|
+
groups.last << attribute_pair(attribute)
|
|
126
|
+
else
|
|
127
|
+
groups << [attribute_pair(attribute)]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
source =
|
|
132
|
+
if groups.length == 1 && !groups.first.is_a?(Array)
|
|
133
|
+
"::RSX::Attributes.render_all(#{groups.first})"
|
|
134
|
+
else
|
|
135
|
+
arguments = groups.map { |group| group.is_a?(Array) ? "{ #{group.join(", ")} }" : "(#{group})" }
|
|
136
|
+
"::RSX::Attributes.render_all(::RSX::Attributes.merge(#{arguments.join(", ")}))"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
parts << [:dynamic, source, attributes.first.line]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def attribute_pair(attribute)
|
|
143
|
+
key = symbol_literal(attribute.name)
|
|
144
|
+
|
|
145
|
+
case attribute.kind
|
|
146
|
+
when :boolean then "#{key} => true"
|
|
147
|
+
when :static then %(#{key} => "#{escape_literal(attribute.value)}")
|
|
148
|
+
when :expression then "#{key} => (#{attribute.value})"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def emit_attribute(attribute, parts)
|
|
153
|
+
name = Attributes.attribute_name(attribute.name)
|
|
154
|
+
return if name.nil? || name.empty?
|
|
155
|
+
|
|
156
|
+
case attribute.kind
|
|
157
|
+
when :boolean
|
|
158
|
+
text = Attributes.boolean?(name) ? " #{name}" : %( #{name}="true")
|
|
159
|
+
parts << [:static, text, attribute.line]
|
|
160
|
+
when :static
|
|
161
|
+
parts << [:static, static_attribute(name, attribute.value), attribute.line]
|
|
162
|
+
when :expression
|
|
163
|
+
parts << [:dynamic, dynamic_attribute(name, attribute.value), attribute.line]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def static_attribute(name, value)
|
|
168
|
+
%( #{name}="#{Escape.static_text(value)}")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def dynamic_attribute(name, source)
|
|
172
|
+
case name
|
|
173
|
+
when "class" then "::RSX::Attributes.render_class(#{source})"
|
|
174
|
+
when "style" then "::RSX::Attributes.render_style(#{source})"
|
|
175
|
+
when "data", "aria" then %(::RSX::Attributes.render_nested("#{name}", #{source}))
|
|
176
|
+
else %(::RSX::Attributes.render("#{name}", #{source}))
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
# Components
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
def emit_component(node, parts)
|
|
185
|
+
source = "::RSX.render_component(#{node.name}, #{props_source(node)}, #{children_source(node)}, self)"
|
|
186
|
+
parts << [:dynamic, source, node.line]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def props_source(node)
|
|
190
|
+
entries = node.attributes.map do |attribute|
|
|
191
|
+
case attribute.kind
|
|
192
|
+
when :spread then "**(#{attribute.value})"
|
|
193
|
+
when :boolean then "#{symbol_literal(attribute.name)} => true"
|
|
194
|
+
when :static then "#{symbol_literal(attribute.name)} => \"#{escape_literal(attribute.value)}\""
|
|
195
|
+
when :expression then "#{symbol_literal(attribute.name)} => (#{attribute.value})"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
entries.empty? ? "nil" : "{ #{entries.join(", ")} }"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def symbol_literal(name)
|
|
203
|
+
name.match?(/\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/) ? ":#{name}" : %(:"#{escape_literal(name)}")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Children are passed lazily so that components can decide whether (and in
|
|
207
|
+
# what context) to render them: caching, context providers and conditional
|
|
208
|
+
# slots all depend on not having rendered them yet. Fully static children
|
|
209
|
+
# skip the wrapper entirely since there is nothing to defer.
|
|
210
|
+
def children_source(node)
|
|
211
|
+
return "nil" if node.children.empty?
|
|
212
|
+
|
|
213
|
+
# A lone {expression} is handed over untouched rather than rendered to a
|
|
214
|
+
# string, so children can be any value: a lambda for a render prop, an
|
|
215
|
+
# array, a model. RSX.child coerces it if the component interpolates it.
|
|
216
|
+
if node.children.length == 1 && node.children.first.is_a?(Nodes::Expression)
|
|
217
|
+
return "::RSX::Children.new { #{node.children.first.source} }"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
parts = []
|
|
221
|
+
node.children.each { |child| emit(child, parts) }
|
|
222
|
+
parts = merge(parts)
|
|
223
|
+
return "nil" if parts.empty?
|
|
224
|
+
|
|
225
|
+
static = parts.all? { |part| part[0] == :static }
|
|
226
|
+
body = assemble(parts, parts.first[2], parts.last[2], static)
|
|
227
|
+
static ? body : "::RSX::Children.new { #{body} }"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Assembly
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def merge(parts)
|
|
235
|
+
merged = []
|
|
236
|
+
parts.each do |part|
|
|
237
|
+
previous = merged.last
|
|
238
|
+
if part[0] == :static && previous && previous[0] == :static
|
|
239
|
+
previous[1] += part[1]
|
|
240
|
+
else
|
|
241
|
+
merged << part.dup
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
merged
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def assemble(parts, start_line, end_line, static)
|
|
248
|
+
buffer = +""
|
|
249
|
+
|
|
250
|
+
# Markup with nothing dynamic in it is built once and then reused from a
|
|
251
|
+
# per-call-site slot, so re-rendering it allocates nothing at all. The
|
|
252
|
+
# literal after `||=` is never evaluated again after the first render.
|
|
253
|
+
buffer << "(::RSX::STATICS[#{static_slot}] ||= " if static
|
|
254
|
+
buffer << (static ? "::RSX.static(" : "::RSX::SafeString.new(")
|
|
255
|
+
state = { line: start_line, open: false }
|
|
256
|
+
|
|
257
|
+
parts.each do |kind, text, line|
|
|
258
|
+
pad(buffer, state, line)
|
|
259
|
+
open_literal(buffer, state)
|
|
260
|
+
|
|
261
|
+
if kind == :static
|
|
262
|
+
buffer << escape_literal(text)
|
|
263
|
+
else
|
|
264
|
+
buffer << '#{' << text << "}"
|
|
265
|
+
state[:line] += text.count("\n")
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
open_literal(buffer, state) if parts.empty?
|
|
270
|
+
close_literal(buffer, state)
|
|
271
|
+
pad(buffer, state, end_line)
|
|
272
|
+
buffer << ")"
|
|
273
|
+
buffer << ")" if static
|
|
274
|
+
buffer
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def static_slot
|
|
278
|
+
@statics += 1
|
|
279
|
+
%(:"#{@prefix}#{@statics}")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Keeps generated Ruby on the same lines as the .rsx source it came from.
|
|
283
|
+
# Inside a string literal, extra lines are added by closing the literal and
|
|
284
|
+
# continuing it: adjacent literals are concatenated by the parser, so no
|
|
285
|
+
# markup is affected.
|
|
286
|
+
def pad(buffer, state, target)
|
|
287
|
+
count = target - state[:line]
|
|
288
|
+
return if count <= 0
|
|
289
|
+
|
|
290
|
+
if state[:open]
|
|
291
|
+
buffer << '" ' << ("\\\n" * count) << '"'
|
|
292
|
+
else
|
|
293
|
+
buffer << ("\n" * count)
|
|
294
|
+
end
|
|
295
|
+
state[:line] = target
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def open_literal(buffer, state)
|
|
299
|
+
return if state[:open]
|
|
300
|
+
|
|
301
|
+
buffer << '"'
|
|
302
|
+
state[:open] = true
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def close_literal(buffer, state)
|
|
306
|
+
return unless state[:open]
|
|
307
|
+
|
|
308
|
+
buffer << '"'
|
|
309
|
+
state[:open] = false
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def escape_literal(text)
|
|
313
|
+
text.gsub(LITERAL_PATTERN) { |match| LITERAL_ESCAPES[match] || "\\#{match}" }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def last_line(parts, node)
|
|
317
|
+
parts.empty? ? node.line : parts.last[2]
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module RSX
|
|
7
|
+
# Caches the Ruby produced from .rsx files on disk, keyed by a digest of the
|
|
8
|
+
# source. A warm cache turns loading a template into reading a .rb file, so
|
|
9
|
+
# booting a large application does no transformation work at all.
|
|
10
|
+
class CompileCache
|
|
11
|
+
attr_reader :directory
|
|
12
|
+
|
|
13
|
+
def initialize(directory)
|
|
14
|
+
@directory = directory
|
|
15
|
+
@enabled = !directory.nil?
|
|
16
|
+
@memory = {}
|
|
17
|
+
@lock = Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def enabled?
|
|
21
|
+
@enabled
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def digest(source)
|
|
25
|
+
Digest::SHA256.hexdigest("#{COMPILER_VERSION}\0#{source}")[0, 32]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the compiled Ruby for source, compiling only on a cache miss.
|
|
29
|
+
def fetch(path, source)
|
|
30
|
+
key = digest(source)
|
|
31
|
+
cached = @lock.synchronize { @memory[key] }
|
|
32
|
+
return cached if cached
|
|
33
|
+
|
|
34
|
+
ruby = read(path, key) || begin
|
|
35
|
+
compiled = yield
|
|
36
|
+
write(path, key, compiled)
|
|
37
|
+
compiled
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@lock.synchronize { @memory[key] = ruby }
|
|
41
|
+
ruby
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def path_for(source_path, key)
|
|
45
|
+
return nil unless enabled?
|
|
46
|
+
|
|
47
|
+
basename = File.basename(source_path.to_s, ".*")
|
|
48
|
+
basename = "template" if basename.empty?
|
|
49
|
+
File.join(@directory, "#{basename}-#{key}.rb")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def read(source_path, key)
|
|
53
|
+
target = path_for(source_path, key)
|
|
54
|
+
return nil unless target && File.file?(target)
|
|
55
|
+
|
|
56
|
+
File.read(target)
|
|
57
|
+
rescue SystemCallError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def write(source_path, key, ruby)
|
|
62
|
+
target = path_for(source_path, key)
|
|
63
|
+
return ruby unless target
|
|
64
|
+
|
|
65
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
66
|
+
temporary = "#{target}.#{Process.pid}.#{rand(1 << 24)}.tmp"
|
|
67
|
+
File.binwrite(temporary, ruby)
|
|
68
|
+
File.rename(temporary, target)
|
|
69
|
+
ruby
|
|
70
|
+
rescue SystemCallError
|
|
71
|
+
# A read-only or missing cache directory must never break rendering.
|
|
72
|
+
@enabled = false
|
|
73
|
+
ruby
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def clear
|
|
77
|
+
@lock.synchronize { @memory.clear }
|
|
78
|
+
return unless enabled? && File.directory?(@directory)
|
|
79
|
+
|
|
80
|
+
Dir.glob(File.join(@directory, "*.rb")).each { |file| File.delete(file) }
|
|
81
|
+
Dir.glob(File.join(@directory, "*.tmp")).each { |file| File.delete(file) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|