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.
@@ -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
@@ -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
@@ -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