Module: RSX

Defined in:
lib/rsx.rb,
lib/rsx/cli.rb,
lib/rsx/cache.rb,
lib/rsx/nodes.rb,
lib/rsx/errors.rb,
lib/rsx/escape.rb,
lib/rsx/loader.rb,
lib/rsx/codegen.rb,
lib/rsx/context.rb,
lib/rsx/helpers.rb,
lib/rsx/railtie.rb,
lib/rsx/version.rb,
lib/rsx/children.rb,
lib/rsx/template.rb,
lib/rsx/component.rb,
lib/rsx/attributes.rb,
lib/rsx/safe_string.rb,
lib/rsx/transformer.rb,
lib/rsx/compile_cache.rb,
lib/rsx/template_handler.rb

Overview

RSX brings JSX authoring to Ruby.

component Greeting do |name:|
return <p className="greeting">Hello, {name}!</p>
end

Markup is compiled ahead of time into Ruby string building, so rendering is just concatenation and escaping. See README.md for the full language guide.

Defined Under Namespace

Modules: Attributes, Cache, Escape, Helpers, Nodes Classes: CLI, Children, Codegen, CompileCache, Component, Configuration, Context, Error, FileNotFoundError, Loader, PropsError, Railtie, SafeString, SyntaxError, Template, TemplateHandler, Transformer, UnknownComponentError

Constant Summary collapse

EMPTY_SAFE =
SafeString.new("").freeze
STATICS =

Slots for markup that never changes, populated on first render by compiled templates. Keys are generated at compile time, one per call site.

{}
VERSION =
"0.1.0"
COMPILER_VERSION =

Bumping this invalidates every on-disk compile cache entry.

"1"

Class Method Summary collapse

Class Method Details

.assign_constant(path, value) ⇒ Object


Constants



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/rsx.rb', line 336

def assign_constant(path, value)
  parts = path.to_s.split("::")
  target = config.component_namespace

  parts[0..-2].each do |part|
    target = if target.const_defined?(part, false)
               target.const_get(part, false)
             else
               target.const_set(part, Module.new)
             end
  end

  last = parts.last
  target.send(:remove_const, last) if target.const_defined?(last, false)
  target.const_set(last, value)
  value
end

.cacheObject



103
104
105
# File 'lib/rsx.rb', line 103

def cache
  config.cache_store
end

.child(value) ⇒ Object

Coerces any value into markup, escaping it unless it is already safe. Mirrors React: nil, true and false render nothing, arrays are concatenated.



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/rsx.rb', line 229

def child(value)
  case value
  when SafeString then value
  when String then Escape.safe?(value) ? value : Escape.html(value)
  when nil, true, false then EMPTY_SAFE
  when Children then value.render
  when Integer, Float, Symbol then value.to_s
  when Array
    out = +""
    value.each { |item| out << child(item) }
    out
  when Component then value.rsx_perform
  when Hash
    raise Error, "cannot render a Hash. Did you mean to interpolate one of its values?"
  else
    if value.respond_to?(:html_safe?) && value.html_safe?
      value.to_s
    elsif value.respond_to?(:to_rsx)
      child(value.to_rsx)
    elsif value.is_a?(Class) && value < Component
      raise Error, "cannot render the component class #{value.name} as a value. " \
                   "Write <#{value.name} /> to render it."
    else
      Escape.html(value.to_s)
    end
  end
end

.compile(source, path: nil) ⇒ Object

Transforms .rsx source into plain Ruby. Useful for debugging: rsx compile.



119
120
121
# File 'lib/rsx.rb', line 119

def compile(source, path: nil)
  Transformer.transform(source, path: path)
end

.configObject


Configuration



90
91
92
# File 'lib/rsx.rb', line 90

def config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Yields:



94
95
96
97
# File 'lib/rsx.rb', line 94

def configure
  yield config
  config
end

.constant_defined?(path) ⇒ Boolean Also known as: const_defined_at?

Returns:

  • (Boolean)


354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/rsx.rb', line 354

def constant_defined?(path)
  parts = path.to_s.split("::")
  return false unless parts.all? { |part| part.match?(/\A[A-Z][A-Za-z0-9_]*\z/) }

  target = config.component_namespace
  parts.each do |part|
    return false unless target.is_a?(Module) && target.const_defined?(part, false)

    target = target.const_get(part, false)
  end
  true
end

.create_context(default = nil, name: nil) ⇒ Object



306
307
308
# File 'lib/rsx.rb', line 306

def create_context(default = nil, name: nil)
  Context.new(default, name: name)
end

.define_component(name, cache: nil, static: false, &body) ⇒ Object

Emitted by component Name do |props| ... end.



296
297
298
299
300
301
302
303
304
# File 'lib/rsx.rb', line 296

def define_component(name, cache: nil, static: false, &body)
  component = Class.new(Component)
  assign_constant(name, component)
  component.cache(cache) if cache
  component.rsx_static! if static
  loader.track(component)
  component.class_eval(&body)
  component
end

.escape(value) ⇒ Object



274
275
276
# File 'lib/rsx.rb', line 274

def escape(value)
  SafeString.new(Escape.html(value.to_s))
end

.export(component, from: nil) ⇒ Object

Emitted by export Name.



324
325
326
327
328
329
330
# File 'lib/rsx.rb', line 324

def export(component, from: nil)
  entry = loader.current_entry || (from && loader.entries.find { |candidate| candidate.path == from })
  if entry && !entry.components.include?(component)
    entry.components << component
  end
  component
end

.export_default(component, from: nil) ⇒ Object

Emitted by export default Name.



317
318
319
320
321
# File 'lib/rsx.rb', line 317

def export_default(component, from: nil)
  entry = loader.current_entry || (from && loader.entries.find { |candidate| candidate.path == from })
  entry.default = component if entry
  component
end

.import(spec, as: nil, from: nil) ⇒ Object

Emitted by import Name from "path".



311
312
313
314
# File 'lib/rsx.rb', line 311

def import(spec, as: nil, from: nil)
  from = nil if from.to_s.empty?
  loader.import(spec, as: as, from: from)
end

.json(object) ⇒ Object



286
287
288
289
# File 'lib/rsx.rb', line 286

def json(object)
  require "json"
  JSON.generate(object)
end

.key_fragment(object) ⇒ Object



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/rsx.rb', line 407

def key_fragment(object)
  case object
  when nil then "~"
  when String then object
  when Symbol, Numeric, true, false then object.to_s
  when Array then object.map { |item| key_fragment(item) }.join(",")
  when Hash then object.map { |key, value| "#{key}=#{key_fragment(value)}" }.join("&")
  when Time then object.to_f.to_s
  else
    if object.respond_to?(:cache_key_with_version)
      object.cache_key_with_version.to_s
    elsif object.respond_to?(:cache_key)
      object.cache_key.to_s
    elsif object.respond_to?(:id) && object.respond_to?(:updated_at)
      "#{object.class}/#{object.id}-#{object.updated_at.to_i}"
    else
      object.to_s
    end
  end
end

.load(path) ⇒ Object



123
124
125
# File 'lib/rsx.rb', line 123

def load(path)
  loader.load(path)
end

.load_all(paths = config.paths) ⇒ Object



127
128
129
# File 'lib/rsx.rb', line 127

def load_all(paths = config.paths)
  loader.load_all(paths)
end

.loaderObject



99
100
101
# File 'lib/rsx.rb', line 99

def loader
  @loader ||= Loader.new(config)
end

.lookup_component(name) ⇒ Object



212
213
214
215
216
217
218
219
220
221
# File 'lib/rsx.rb', line 212

def lookup_component(name)
  name = name.to_s
  namespace = config.component_namespace
  return namespace.const_get(name) if constant_defined?(name)

  resolved = loader.resolve(name)
  return loader.default_export(resolved) if resolved

  raise UnknownComponentError, "no component named #{name}"
end

.normalize_cache_options(options) ⇒ Object


Cache helpers



390
391
392
393
394
395
396
397
398
399
# File 'lib/rsx.rb', line 390

def normalize_cache_options(options)
  case options
  when true then {}
  when false, nil then nil
  when Hash then options
  when Numeric then { expires_in: options }
  when Proc then { key: options }
  else raise ArgumentError, "unsupported cache option #{options.inspect}"
  end
end

.precompile!(paths = config.paths) ⇒ Object

Compiles every .rsx file under the configured paths and warms the on-disk cache. Run at boot (or from rake rsx:precompile) so requests never compile anything.



138
139
140
141
142
143
# File 'lib/rsx.rb', line 138

def precompile!(paths = config.paths)
  loader.files(paths).each do |file|
    source = File.read(file)
    config.compile_cache.fetch(file, source) { Transformer.transform(source, path: file) }
  end
end

.raw(value) ⇒ Object

Marks a string as trusted HTML. The equivalent of Rails' html_safe.



268
269
270
271
272
# File 'lib/rsx.rb', line 268

def raw(value)
  return EMPTY_SAFE if value.nil?

  value.is_a?(SafeString) ? value : SafeString.new(value.to_s)
end

.raw_html(value) ⇒ Object

dangerouslySetInnerHTML=__html: markup }



279
280
281
282
283
284
# File 'lib/rsx.rb', line 279

def raw_html(value)
  return EMPTY_SAFE if value.nil?

  html = value.respond_to?(:[]) && !value.is_a?(String) ? (value[:__html] || value["__html"]) : value
  html.nil? ? EMPTY_SAFE : html.to_s
end

.reload!Object



131
132
133
# File 'lib/rsx.rb', line 131

def reload!
  loader.reload!
end

.remove_constant(component) ⇒ Object



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/rsx.rb', line 368

def remove_constant(component)
  name = component.name
  return if name.nil?

  parts = name.split("::")
  target = Object
  parts[0..-2].each do |part|
    return unless target.const_defined?(part, false)

    target = target.const_get(part, false)
  end
  last = parts.last
  return unless target.const_defined?(last, false)
  return unless target.const_get(last, false).equal?(component)

  target.send(:remove_const, last)
end

.render(target, context: nil, **props) ⇒ Object

Renders a component, a .rsx file path, or a lambda component.

RSX.render(UserProfile, user: user)
RSX.render("app/components/user_profile.rsx", user: user)


159
160
161
162
163
164
165
# File 'lib/rsx.rb', line 159

def render(target, context: nil, **props)
  if target.is_a?(String) || target.respond_to?(:to_path)
    render_file(target.to_s, context: context, **props)
  else
    safe(render_component(target, props.empty? ? nil : props, nil, context))
  end
end

.render_component(target, props = nil, children = nil, parent = nil) ⇒ Object

Internal: emitted by compiled markup for every tag.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/rsx.rb', line 190

def render_component(target, props = nil, children = nil, parent = nil)
  if children
    props = props ? props : {}
    props[:children] = children
  end

  case target
  when Proc
    child(target.arity.zero? ? target.call : target.call(props || Component::EMPTY_PROPS))
  when String, Symbol
    render_component(lookup_component(target), props, nil, parent)
  else
    unless target.respond_to?(:rsx_call)
      raise UnknownComponentError,
            "#{target.inspect} is not renderable. Components are defined with " \
            "`component Name do ... end` in a .rsx file."
    end

    target.rsx_call(props, parent)
  end
end

.render_file(path, context: nil, **props) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
# File 'lib/rsx.rb', line 167

def render_file(path, context: nil, **props)
  entry = loader.load(loader.resolve!(path))
  props = props.empty? ? nil : props
  component = entry.renderable

  if component
    safe(render_component(component, props, nil, context))
  else
    entry.template.render(props, context: context)
  end
end

.render_source(source, path: nil, context: nil, **props) ⇒ Object

Renders .rsx source directly. Handy in tests and scripts.



180
181
182
# File 'lib/rsx.rb', line 180

def render_source(source, path: nil, context: nil, **props)
  Template.new(source, path: path).render(props.empty? ? nil : props, context: context)
end

.reset!Object



107
108
109
110
111
112
# File 'lib/rsx.rb', line 107

def reset!
  @config = nil
  @loader = nil
  @templates = nil
  STATICS.clear
end

.safe(value) ⇒ Object

Wraps an already-escaped or trusted string without copying when possible.



263
264
265
# File 'lib/rsx.rb', line 263

def safe(value)
  value.is_a?(SafeString) ? value : SafeString.new(value.to_s)
end

.stable_key(object) ⇒ Object

Builds a short, stable cache key fragment from arbitrary Ruby values.



402
403
404
405
# File 'lib/rsx.rb', line 402

def stable_key(object)
  fragment = key_fragment(object)
  fragment.length > 128 ? Digest::SHA256.hexdigest(fragment)[0, 32] : fragment
end

.static(literal) ⇒ Object

Wraps markup that the compiler proved static. Called once per call site.



258
259
260
# File 'lib/rsx.rb', line 258

def static(literal)
  SafeString.new(literal).freeze
end

.template(path) ⇒ Object

The compiled Template for a markup .rsx file.



146
147
148
149
# File 'lib/rsx.rb', line 146

def template(path)
  entry = loader.load(loader.resolve!(path))
  entry.template || raise(Error, "#{entry.path} defines components; render one of them instead")
end

.template_result(value) ⇒ Object

Internal: wraps the value of a compiled ActionView template.



185
186
187
# File 'lib/rsx.rb', line 185

def template_result(value)
  safe(child(value))
end