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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSX
|
|
4
|
+
# An HTML-safe string. Anything wrapped in a SafeString is emitted verbatim;
|
|
5
|
+
# everything else that flows into a template is escaped.
|
|
6
|
+
#
|
|
7
|
+
# SafeString subclasses String so it interoperates with ActionView output
|
|
8
|
+
# buffers, ActiveSupport::SafeBuffer and plain Ruby string operations without
|
|
9
|
+
# requiring ActiveSupport to be loaded.
|
|
10
|
+
class SafeString < String
|
|
11
|
+
def html_safe?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Rails calls this when concatenating into an output buffer.
|
|
16
|
+
def to_s
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_safe_string
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Concatenation keeps the safety contract: unsafe operands are escaped.
|
|
25
|
+
def +(other)
|
|
26
|
+
SafeString.new(super(RSX.child(other)))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def <<(other)
|
|
30
|
+
super(RSX.child(other))
|
|
31
|
+
end
|
|
32
|
+
alias concat <<
|
|
33
|
+
|
|
34
|
+
def *(times)
|
|
35
|
+
SafeString.new(super)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def inspect
|
|
39
|
+
"#<RSX::SafeString #{super}>"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/rsx/tasks.rake
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :rsx do
|
|
4
|
+
desc "Compile every .rsx file and warm the on-disk compile cache"
|
|
5
|
+
task precompile: :environment do
|
|
6
|
+
files = RSX.loader.files
|
|
7
|
+
RSX.precompile!
|
|
8
|
+
puts "rsx: precompiled #{files.length} file(s) into #{RSX.config.cache_dir || "memory"}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Delete compiled .rsx output and clear the render cache"
|
|
12
|
+
task clear: :environment do
|
|
13
|
+
RSX.config.compile_cache.clear
|
|
14
|
+
RSX.cache.clear
|
|
15
|
+
puts "rsx: cleared #{RSX.config.cache_dir || "memory"}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Load every .rsx file and report the components it defines"
|
|
19
|
+
task components: :environment do
|
|
20
|
+
RSX.load_all
|
|
21
|
+
RSX.loader.entries.sort_by(&:path).each do |entry|
|
|
22
|
+
names = entry.components.map { |component| component.name || "(anonymous)" }
|
|
23
|
+
puts format("%-60s %s", entry.path.sub("#{Dir.pwd}/", ""), names.join(", "))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/rsx/template.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSX
|
|
4
|
+
# A .rsx file rendered as markup rather than as a named component.
|
|
5
|
+
#
|
|
6
|
+
# The file body becomes a method on an anonymous Component subclass, so a
|
|
7
|
+
# template gets the whole component toolkit (children, caching, context,
|
|
8
|
+
# helpers) and is compiled once no matter how often it is rendered.
|
|
9
|
+
class Template
|
|
10
|
+
attr_reader :path, :digest
|
|
11
|
+
|
|
12
|
+
def self.load(path)
|
|
13
|
+
absolute = File.expand_path(path)
|
|
14
|
+
raise FileNotFoundError, "no such RSX file: #{absolute}" unless File.file?(absolute)
|
|
15
|
+
|
|
16
|
+
new(File.read(absolute), path: absolute)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_ruby(ruby, path: nil, digest: nil)
|
|
20
|
+
allocate.tap { |template| template.send(:build, ruby, path, digest) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(source, path: nil)
|
|
24
|
+
digest = RSX.config.compile_cache.digest(source.to_s)
|
|
25
|
+
ruby = RSX.config.compile_cache.fetch(path || "template", source.to_s) do
|
|
26
|
+
Transformer.transform(source.to_s, path: path)
|
|
27
|
+
end
|
|
28
|
+
build(ruby, path, digest)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(props = nil, context: nil)
|
|
32
|
+
value = @component.new(props, context).rsx_render(props)
|
|
33
|
+
|
|
34
|
+
# A file whose last statement is a component definition (or an
|
|
35
|
+
# `export default`) renders that component with the props it was given.
|
|
36
|
+
if value.is_a?(Class) && value < Component
|
|
37
|
+
RSX.safe(value.rsx_call(props, context))
|
|
38
|
+
else
|
|
39
|
+
RSX.safe(RSX.child(value))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def component
|
|
44
|
+
@component
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build(ruby, path, digest)
|
|
50
|
+
@path = path
|
|
51
|
+
@digest = digest
|
|
52
|
+
@component = Class.new(Component)
|
|
53
|
+
@component.rsx_source_path = path
|
|
54
|
+
@component.rsx_source_digest = digest
|
|
55
|
+
|
|
56
|
+
body = ruby.end_with?("\n") ? ruby : "#{ruby}\n"
|
|
57
|
+
# Evaluating from line 0 puts the generated `def` above line 1, so every
|
|
58
|
+
# line of the template keeps its original number in backtraces.
|
|
59
|
+
@component.class_eval("def rsx_render(props = nil)\n#{body}end\n", path || "(rsx)", 0)
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSX
|
|
4
|
+
# ActionView handler for .rsx templates: app/views/users/show.html.rsx
|
|
5
|
+
#
|
|
6
|
+
# The handler returns Ruby source, which ActionView compiles into a method on
|
|
7
|
+
# the view context. That means a .rsx view is compiled once per process and
|
|
8
|
+
# every Rails helper (url helpers, `t`, form builders, CSRF tags) is available
|
|
9
|
+
# directly, because `self` inside the template is the view.
|
|
10
|
+
class TemplateHandler
|
|
11
|
+
class << self
|
|
12
|
+
# ActionView passes (template) on older versions and (template, source)
|
|
13
|
+
# on 6.0+.
|
|
14
|
+
def call(template, source = nil)
|
|
15
|
+
source ||= template.source
|
|
16
|
+
identifier = template.respond_to?(:identifier) ? template.identifier : nil
|
|
17
|
+
compile(source, identifier)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def compile(source, identifier = nil)
|
|
21
|
+
ruby = RSX.compile(source, path: identifier)
|
|
22
|
+
|
|
23
|
+
# Everything is kept on one line up to the body so that line numbers in
|
|
24
|
+
# the compiled method still match the .rsx file.
|
|
25
|
+
+"__rsx_value = begin;" << ruby << "\nend\n::RSX.template_result(__rsx_value)\n"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Templates render markup, not a component, so there is nothing to escape
|
|
29
|
+
# on the way out.
|
|
30
|
+
def handles_encoding?
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|