compex 0.1.1
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/.rspec +3 -0
- data/.rubocop.yml +69 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/lib/compex/asset_provider.rb +22 -0
- data/lib/compex/bag.rb +25 -0
- data/lib/compex/base.rb +219 -0
- data/lib/compex/cache/base.rb +10 -0
- data/lib/compex/cache/memcached_backend.rb +19 -0
- data/lib/compex/cache/memory_backend.rb +17 -0
- data/lib/compex/cache/noop_backend.rb +10 -0
- data/lib/compex/cache/redis_backend.rb +19 -0
- data/lib/compex/cache.rb +74 -0
- data/lib/compex/component_registry.rb +81 -0
- data/lib/compex/config.rb +81 -0
- data/lib/compex/js_rewriter.rb +18 -0
- data/lib/compex/string_refinements.rb +15 -0
- data/lib/compex/style.rb +91 -0
- data/lib/compex/template/assembler.rb +174 -0
- data/lib/compex/template/renderer.rb +153 -0
- data/lib/compex/template.rb +16 -0
- data/lib/compex/unsafe_string.rb +19 -0
- data/lib/compex/version.rb +5 -0
- data/lib/compex.rb +38 -0
- metadata +116 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
class ComponentRegistry
|
|
5
|
+
Node = Struct.new(:name, :children, :component) do
|
|
6
|
+
def initialize(*)
|
|
7
|
+
super
|
|
8
|
+
self.children ||= []
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
attr_reader :components, :tree
|
|
14
|
+
|
|
15
|
+
def register(component)
|
|
16
|
+
@components ||= {}
|
|
17
|
+
@tree ||= Struct.new(:children).new(children: [])
|
|
18
|
+
|
|
19
|
+
stable_id = CompEx::Base.stable_class_id(component)
|
|
20
|
+
@components[stable_id] = component
|
|
21
|
+
|
|
22
|
+
names = stable_id.split("::")
|
|
23
|
+
node = @tree
|
|
24
|
+
until names.empty?
|
|
25
|
+
name = names.shift
|
|
26
|
+
target = node.children.find { it.name == name }
|
|
27
|
+
unless target
|
|
28
|
+
target = Node.new(name)
|
|
29
|
+
node.children << target
|
|
30
|
+
end
|
|
31
|
+
if names.empty?
|
|
32
|
+
warn "[CompEx] Component name collision: #{component.name} vs #{target.component.name}" if target.component
|
|
33
|
+
target.component = component
|
|
34
|
+
end
|
|
35
|
+
node = target
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def find_by_path(path, base = nil)
|
|
40
|
+
base ||= @tree
|
|
41
|
+
return base if path.empty?
|
|
42
|
+
|
|
43
|
+
ret = base.children.find { it.name == path.first }
|
|
44
|
+
return nil unless ret
|
|
45
|
+
|
|
46
|
+
find_by_path(path[1...], ret)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolve(name, context: nil)
|
|
50
|
+
return nil if name.nil? || name.empty?
|
|
51
|
+
|
|
52
|
+
# Normalize
|
|
53
|
+
absolute = name.start_with?("::")
|
|
54
|
+
path = name.gsub(/^::/, "").split("::")
|
|
55
|
+
|
|
56
|
+
# Absolute lookup always from root
|
|
57
|
+
if absolute
|
|
58
|
+
node = find_by_path(path, @tree)
|
|
59
|
+
return node&.component
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Relative lookup: walk up context nesting
|
|
63
|
+
if context
|
|
64
|
+
namespaces = context.name.split("::")
|
|
65
|
+
until namespaces.empty?
|
|
66
|
+
base = find_by_path(namespaces, @tree)
|
|
67
|
+
if base
|
|
68
|
+
node = find_by_path(path, base)
|
|
69
|
+
return node&.component if node&.component
|
|
70
|
+
end
|
|
71
|
+
namespaces.pop
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Fallback: top-level
|
|
76
|
+
node = find_by_path(path, @tree)
|
|
77
|
+
node&.component
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
CONFIG_MU = Mutex.new
|
|
7
|
+
|
|
8
|
+
def configure(&)
|
|
9
|
+
CONFIG_MU.synchronize do
|
|
10
|
+
@config ||= Config.new
|
|
11
|
+
yield @config if block_given?
|
|
12
|
+
@config
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def config = configure
|
|
17
|
+
|
|
18
|
+
class Config
|
|
19
|
+
attr_accessor :on_multiple_root, :multiple_root_wrap_element,
|
|
20
|
+
:on_non_element, :cache_args, :cache_kwargs, :cache_prefix,
|
|
21
|
+
:template_search_path, :js_search_path, :style_search_path
|
|
22
|
+
attr_reader :cache_mode, :cache_backend
|
|
23
|
+
|
|
24
|
+
CACHE_MODES = {
|
|
25
|
+
memcached: Cache::MemcachedBackend,
|
|
26
|
+
redis: Cache::RedisBackend,
|
|
27
|
+
memory: Cache::MemoryBackend,
|
|
28
|
+
disabled: Cache::NoopBackend
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@on_multiple_root = :wrap
|
|
33
|
+
@multiple_root_wrap_element = "div"
|
|
34
|
+
@on_non_element = :wrap
|
|
35
|
+
self.cache_mode = :memory
|
|
36
|
+
|
|
37
|
+
return unless defined?(Rails)
|
|
38
|
+
|
|
39
|
+
root_path = File.join(Rails.root, "app", "views", "components")
|
|
40
|
+
@template_search_path = root_path
|
|
41
|
+
@style_search_path = root_path
|
|
42
|
+
@js_search_path = root_path
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cache_mode=(value)
|
|
46
|
+
value = value.to_sym if value.is_a? String
|
|
47
|
+
raise ArgumentError, "Unknown cache mode #{value}, valid options are #{CACHE_MODES.keys.map(&:to_s).join(", ")}" unless CACHE_MODES.key? value
|
|
48
|
+
|
|
49
|
+
@cache_backend = CACHE_MODES[value].new
|
|
50
|
+
@cache_backend.prepare!(*cache_args, **cache_kwargs)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def lookup_template(component, type)
|
|
54
|
+
@lookup_cache ||= {}
|
|
55
|
+
lookup_key = [component, type]
|
|
56
|
+
return @lookup_cache[lookup_key] if @lookup_cache.key? lookup_key
|
|
57
|
+
|
|
58
|
+
cname = component.component_name
|
|
59
|
+
raise MissingTemplate, "No template defined by anonymous class #{self.class} (through ::#{type})" if type == :html && !cname
|
|
60
|
+
|
|
61
|
+
# Anonymous classes can't be looked up since we can't define its path
|
|
62
|
+
unless cname
|
|
63
|
+
@lookup_cache[lookup_key] = nil
|
|
64
|
+
return nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ret = case type
|
|
68
|
+
when :template
|
|
69
|
+
File.join(@template_search_path || "", "#{cname}.html.crb")
|
|
70
|
+
when :style
|
|
71
|
+
File.join(@style_search_path || "", "#{cname}.css")
|
|
72
|
+
when :js
|
|
73
|
+
File.join(@js_search_path || "", "#{cname}.js")
|
|
74
|
+
else
|
|
75
|
+
raise "Unexpected lookup type #{type.inspect}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@lookup_cache[lookup_key] = ret
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
class JSRewriter
|
|
5
|
+
def self.rewrite(component, raw_js)
|
|
6
|
+
return nil unless raw_js
|
|
7
|
+
|
|
8
|
+
class_name = component.js_class_id
|
|
9
|
+
source = raw_js.strip.gsub(/\Aclass\s+[^\s]+\s*{/, "").gsub(/}\z/, "")
|
|
10
|
+
<<~JS
|
|
11
|
+
((Runtime, Component) => {
|
|
12
|
+
class #{class_name} extends Component {#{source}}
|
|
13
|
+
Runtime.register(#{class_name.inspect}, #{class_name});
|
|
14
|
+
})(window.CompEx.Runtime, window.CompEx.Component);
|
|
15
|
+
JS
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
module StringRefinements
|
|
5
|
+
refine String do
|
|
6
|
+
def underscore
|
|
7
|
+
gsub("::", "/")
|
|
8
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
9
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
10
|
+
.tr("-", "_")
|
|
11
|
+
.downcase
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/compex/style.rb
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
class Style
|
|
5
|
+
def self.compile(component_class, root_classes = nil)
|
|
6
|
+
[
|
|
7
|
+
component_prefix(component_class),
|
|
8
|
+
new(component_class, root_classes).compile
|
|
9
|
+
]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.component_prefix(component) = "cx_#{hash_string(comp_id(component))}"
|
|
13
|
+
|
|
14
|
+
def self.comp_id(comp) = comp.class.name || comp.class.hash.to_s(36)
|
|
15
|
+
|
|
16
|
+
def self.hash_string(str)
|
|
17
|
+
value = 2906
|
|
18
|
+
str.chars.map(&:ord).each do |v|
|
|
19
|
+
value = (value * 33) ^ v
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
(value & 0xFFFFFFFF).to_s(36)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(klass, root_classes = nil)
|
|
26
|
+
@comp = klass
|
|
27
|
+
@style = klass.style
|
|
28
|
+
@ast = MiniCSS.parse(@style)
|
|
29
|
+
@root_classes = root_classes || []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def id_for_component = self.class.component_prefix(@comp)
|
|
33
|
+
|
|
34
|
+
def compile
|
|
35
|
+
@ast.each do |v|
|
|
36
|
+
case v
|
|
37
|
+
when MiniCSS::AST::Rule
|
|
38
|
+
next unless v.valid_selector?
|
|
39
|
+
|
|
40
|
+
v.selector = prefix_selector(v.selector)
|
|
41
|
+
when MiniCSS::AST::AtRule
|
|
42
|
+
next unless %w[media supports].include?(v.name)
|
|
43
|
+
|
|
44
|
+
v.child_rules = v.child_rules.map do |rule|
|
|
45
|
+
next rule unless rule.is_a? MiniCSS::AST::Rule
|
|
46
|
+
next rule unless rule.valid_selector?
|
|
47
|
+
|
|
48
|
+
rule.selector = prefix_selector(rule.selector)
|
|
49
|
+
rule
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
raise "Unexpected AST element #{v}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
MiniCSS.serialize(@ast)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def prefix_selector(sel)
|
|
59
|
+
case sel[:type]
|
|
60
|
+
when :class
|
|
61
|
+
{
|
|
62
|
+
type: :complex,
|
|
63
|
+
combinator: @root_classes.include?(sel[:content].gsub(/^\./, "")) ? "" : " ",
|
|
64
|
+
left: { type: :class, content: ".#{id_for_component}" },
|
|
65
|
+
right: sel
|
|
66
|
+
}
|
|
67
|
+
when :type, :attribute, :universal, :id
|
|
68
|
+
{
|
|
69
|
+
type: :complex,
|
|
70
|
+
combinator: " ",
|
|
71
|
+
left: { type: :class, content: ".#{id_for_component}" },
|
|
72
|
+
right: sel
|
|
73
|
+
}
|
|
74
|
+
when :list
|
|
75
|
+
sel.tap do |s|
|
|
76
|
+
s[:list].map! { prefix_selector(it) }
|
|
77
|
+
end
|
|
78
|
+
when :complex
|
|
79
|
+
sel.tap do |s|
|
|
80
|
+
s[:left] = prefix_selector(s[:left])
|
|
81
|
+
end
|
|
82
|
+
when :compound
|
|
83
|
+
sel.tap do |s|
|
|
84
|
+
s[:list][0] = prefix_selector(s[:list].first)
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
raise "Unexpected selector type #{sel[:type]}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
class Template
|
|
5
|
+
class Assembler
|
|
6
|
+
HTML_VOID_TAG = %w[area base br col embed hr img input link meta param source track wbr].freeze
|
|
7
|
+
ELEMENTS_WITH_SELF_CLOSING_CHILDREN = %w[svg math].freeze
|
|
8
|
+
|
|
9
|
+
attr_accessor :context_id, :embeddings, :components, :result
|
|
10
|
+
|
|
11
|
+
def initialize(ast, context_id)
|
|
12
|
+
@ast = ast
|
|
13
|
+
@embeddings = {}
|
|
14
|
+
@components = {}
|
|
15
|
+
@result = []
|
|
16
|
+
@literal = +""
|
|
17
|
+
@context_id = context_id
|
|
18
|
+
@embedding_id = 0
|
|
19
|
+
@component_id = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def assemble
|
|
23
|
+
@ast.each { assemble_one(it) }
|
|
24
|
+
push_literal
|
|
25
|
+
@result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def push_literal
|
|
29
|
+
return if @literal.empty?
|
|
30
|
+
|
|
31
|
+
@result << { kind: :literal, value: @literal }
|
|
32
|
+
@literal = +""
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
COMPONENT_NAME_PATTERN = /^(::)?[A-Z]\w*+(::[A-Z]\w*)*$/
|
|
36
|
+
|
|
37
|
+
def component?(node) = COMPONENT_NAME_PATTERN.match?(node.name)
|
|
38
|
+
|
|
39
|
+
def make_component(node)
|
|
40
|
+
name = "compex-component-#{@component_id}"
|
|
41
|
+
@component_id += 1
|
|
42
|
+
@components[name] = node.name
|
|
43
|
+
name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def assemble_one(node)
|
|
47
|
+
case node
|
|
48
|
+
when MiniHTML::AST::PlainText
|
|
49
|
+
@literal << node.literal
|
|
50
|
+
when MiniHTML::AST::Tag
|
|
51
|
+
name = component?(node) ? make_component(node) : node.name
|
|
52
|
+
@literal << "<#{name}"
|
|
53
|
+
has_attrs = !node.attributes.empty?
|
|
54
|
+
if has_attrs
|
|
55
|
+
@literal << " "
|
|
56
|
+
process_custom_events(node)
|
|
57
|
+
node.attributes.each { assemble_attr(it) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if node.self_closing? && (@allow_self_closing || HTML_VOID_TAG.include?(name))
|
|
61
|
+
@literal << " " if has_attrs
|
|
62
|
+
@literal << "/>"
|
|
63
|
+
return
|
|
64
|
+
elsif node.self_closing?
|
|
65
|
+
@literal << "></#{name}>"
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@literal << ">"
|
|
70
|
+
|
|
71
|
+
if ELEMENTS_WITH_SELF_CLOSING_CHILDREN.include?(name)
|
|
72
|
+
assemble_children_self_closing(node)
|
|
73
|
+
else
|
|
74
|
+
node.children.each { assemble_one(it) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@literal << "</#{name}>"
|
|
78
|
+
|
|
79
|
+
when MiniHTML::AST::Executable
|
|
80
|
+
push_literal
|
|
81
|
+
id = "__cx_embedding_#{@context_id}_#{@embedding_id}__"
|
|
82
|
+
@embedding_id += 1
|
|
83
|
+
@embeddings[id] = node.source
|
|
84
|
+
@result << { kind: :executable, id:, source: node.source }
|
|
85
|
+
else
|
|
86
|
+
raise "Unexpected node type #{node.class}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def assemble_attr(at)
|
|
91
|
+
@literal << at.name
|
|
92
|
+
if at.value.nil?
|
|
93
|
+
@literal << " "
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
@literal << "="
|
|
97
|
+
case at.value
|
|
98
|
+
when MiniHTML::AST::String
|
|
99
|
+
@literal << at.value.quote
|
|
100
|
+
@literal << at.value.literal
|
|
101
|
+
@literal << at.value.quote
|
|
102
|
+
|
|
103
|
+
when MiniHTML::AST::Executable
|
|
104
|
+
push_literal
|
|
105
|
+
id = @embedding_id
|
|
106
|
+
@embedding_id += 1
|
|
107
|
+
@result << { kind: :executable, id: "__cx_embedding_quoted_#{@context_id}_#{id}__", source: at.value.source }
|
|
108
|
+
|
|
109
|
+
when MiniHTML::AST::Interpolation
|
|
110
|
+
assemble_interpolation(at.value)
|
|
111
|
+
|
|
112
|
+
when MiniHTML::AST::Literal
|
|
113
|
+
@literal << at.value.value
|
|
114
|
+
|
|
115
|
+
else
|
|
116
|
+
raise "Unexpected attr value node type #{at.value.class}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def assemble_children_self_closing(node)
|
|
121
|
+
old_self_closing = @allow_self_closing
|
|
122
|
+
@allow_self_closing = true
|
|
123
|
+
node.children.each { assemble_one(it) }
|
|
124
|
+
ensure
|
|
125
|
+
@allow_self_closing = old_self_closing
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def assemble_interpolation(int)
|
|
129
|
+
@literal << int.values.first.quote
|
|
130
|
+
@literal << int.values.first.literal
|
|
131
|
+
|
|
132
|
+
int.values[1...].each do |v|
|
|
133
|
+
case v
|
|
134
|
+
when MiniHTML::AST::String
|
|
135
|
+
@literal << v.literal
|
|
136
|
+
when MiniHTML::AST::Executable
|
|
137
|
+
push_literal
|
|
138
|
+
id = @embedding_id
|
|
139
|
+
@embedding_id += 1
|
|
140
|
+
@result << { kind: :executable, id: "__cx_embedding_#{@context_id}_#{id}__", source: v.source }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
@literal << int.values.first.quote
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def process_custom_events(node)
|
|
148
|
+
events = []
|
|
149
|
+
to_remove = []
|
|
150
|
+
node.attributes.each do |at|
|
|
151
|
+
next unless at.name.start_with?("cx-on:")
|
|
152
|
+
|
|
153
|
+
name = at.name.split(":", 2).last
|
|
154
|
+
next unless name
|
|
155
|
+
|
|
156
|
+
handler = at.value.literal
|
|
157
|
+
to_remove << at.name
|
|
158
|
+
if handler.include? ","
|
|
159
|
+
warn "[CompEx] Ignoring custom event definition containing commas: #{at.name}=#{at.value.inspect}"
|
|
160
|
+
next
|
|
161
|
+
end
|
|
162
|
+
events << { name:, handler: }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
node.attributes.reject! { to_remove.include? it.name }
|
|
166
|
+
return if events.empty?
|
|
167
|
+
|
|
168
|
+
@literal << "cx-custom-handlers="
|
|
169
|
+
@literal << events.map { "#{it[:name]}->#{it[:handler]}" }.join(",").inspect
|
|
170
|
+
@literal << " "
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
class Template
|
|
5
|
+
class Renderer
|
|
6
|
+
# The following attributes will have their values merged when we
|
|
7
|
+
# find the same tag already defined by an embedded component
|
|
8
|
+
MERGEABLE_ATTRIBUTES = Set.new(%w[class])
|
|
9
|
+
|
|
10
|
+
attr_accessor :tokens, :embeddings, :frag, :root_classes
|
|
11
|
+
|
|
12
|
+
def initialize(assembler, context)
|
|
13
|
+
@tokens = assembler.result
|
|
14
|
+
@embeddings = assembler.embeddings
|
|
15
|
+
@inner_components = assembler.components
|
|
16
|
+
@frag = nil
|
|
17
|
+
@root_classes = []
|
|
18
|
+
@context = context
|
|
19
|
+
@context_id = @context.component_id
|
|
20
|
+
prepare_template
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def prepare_template
|
|
24
|
+
embeddings.transform_values! do |v|
|
|
25
|
+
RubyVM::InstructionSequence.compile("-> { #{v} }").eval
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
source = @tokens.map do |item|
|
|
29
|
+
case item[:kind]
|
|
30
|
+
when :literal
|
|
31
|
+
item[:value]
|
|
32
|
+
when :executable
|
|
33
|
+
item[:id]
|
|
34
|
+
else
|
|
35
|
+
raise "[BUG] Unexpected token kind #{item[:kind]}"
|
|
36
|
+
end
|
|
37
|
+
end.join
|
|
38
|
+
|
|
39
|
+
@frag = Nokogiri::HTML5.fragment(source.strip)
|
|
40
|
+
|
|
41
|
+
return if @frag.children.empty?
|
|
42
|
+
|
|
43
|
+
if @frag.children.length > 1
|
|
44
|
+
handle_multiple_root
|
|
45
|
+
elsif !@frag.children.first.element?
|
|
46
|
+
handle_non_element
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@root_classes = @frag.children.first.classes.uniq
|
|
50
|
+
handle_inner_components
|
|
51
|
+
@frag
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle_inner_components
|
|
55
|
+
walk(@frag.children.first) do |node|
|
|
56
|
+
next unless node.name.start_with? "compex-component-"
|
|
57
|
+
|
|
58
|
+
inner_name = @inner_components[node.name]
|
|
59
|
+
next unless inner_name
|
|
60
|
+
|
|
61
|
+
c_node = CompEx::ComponentRegistry.resolve(inner_name, context: @context)
|
|
62
|
+
next unless c_node
|
|
63
|
+
|
|
64
|
+
args = c_node.defined_args.map(&:to_s)
|
|
65
|
+
component_props = node.attributes.slice(*args)
|
|
66
|
+
component_props.each_key { node.remove_attribute(it) }
|
|
67
|
+
child = node.children
|
|
68
|
+
replacement = Nokogiri::HTML5.fragment("<#{node.name}></#{node.name}>")
|
|
69
|
+
node.attributes.each_value { replacement.set_attribute(it.name, it.value) }
|
|
70
|
+
node.replace(replacement)
|
|
71
|
+
@inner_components[node.name] = {
|
|
72
|
+
name: node.name,
|
|
73
|
+
attrs: node.attributes.values,
|
|
74
|
+
children: child,
|
|
75
|
+
args: component_props.values.to_h { [it.name.to_sym, it.value] },
|
|
76
|
+
component: c_node
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def walk(node, &)
|
|
82
|
+
return unless node.element?
|
|
83
|
+
|
|
84
|
+
yield node # give caller a chance to handle/replace this node
|
|
85
|
+
|
|
86
|
+
node.children.each do |child|
|
|
87
|
+
walk(child, &)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def define_css_prefix(prefix)
|
|
92
|
+
@frag.children.first.add_class(prefix)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def define_js_module(name)
|
|
96
|
+
@frag.children.first.set_attribute("cx-controller", name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def render(&)
|
|
100
|
+
dom = @frag.dup
|
|
101
|
+
@inner_components.each do |k, v|
|
|
102
|
+
embed = dom.at_css(k)
|
|
103
|
+
next unless embed
|
|
104
|
+
|
|
105
|
+
result = Nokogiri::HTML5.fragment(yield v).children.first
|
|
106
|
+
v[:attrs].each do |attr|
|
|
107
|
+
val = attr.value
|
|
108
|
+
val = "#{result.get_attribute(attr.name)} #{val}" if MERGEABLE_ATTRIBUTES.include?(attr.name) && result.has_attribute?(attr.name)
|
|
109
|
+
result.set_attribute(attr.name, val)
|
|
110
|
+
end
|
|
111
|
+
embed.replace(result)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@embeddings.each do |k, v|
|
|
115
|
+
next if k.start_with? "__cx_embedding"
|
|
116
|
+
|
|
117
|
+
embed = dom.at_css("compex-embedding[id=\"#{k}\"]")
|
|
118
|
+
next unless embed
|
|
119
|
+
|
|
120
|
+
result = yield v
|
|
121
|
+
embed.replace(result)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
dom.to_html
|
|
125
|
+
.gsub(/__cx_embedding_#{@context_id}_\d+__/) { process_embeddings(it, false, &) }
|
|
126
|
+
.gsub(/__cx_embedding_quoted_#{@context_id}_\d+__/) { process_embeddings(it, true, &) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def process_embeddings(key, quoted)
|
|
130
|
+
return key unless @embeddings.key? key
|
|
131
|
+
|
|
132
|
+
ret = yield @embeddings[key]
|
|
133
|
+
ret = ret.inspect if quoted
|
|
134
|
+
ret
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def handle_multiple_root
|
|
138
|
+
raise MultipleRootError, "Element contains multiple root elements" if CompEx.config.on_multiple_root == :raise
|
|
139
|
+
|
|
140
|
+
wrapper = @frag.document.create_element(CompEx.config.multiple_root_wrap_element)
|
|
141
|
+
@frag.children.each { wrapper << it }
|
|
142
|
+
@frag.add_child(wrapper)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def handle_non_element
|
|
146
|
+
raise NonElementRootError, "Element contains multiple root elements" if CompEx.config.on_non_element == :raise
|
|
147
|
+
|
|
148
|
+
wrapper = @frag.document.create_element(CompEx.config.multiple_root_wrap_element)
|
|
149
|
+
@frag.children.first.wrap(wrapper)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "template/renderer"
|
|
4
|
+
require_relative "template/assembler"
|
|
5
|
+
|
|
6
|
+
module CompEx
|
|
7
|
+
class Template
|
|
8
|
+
def self.parse(value, context)
|
|
9
|
+
p = MiniHTML::Parser.new(value)
|
|
10
|
+
ast = p.parse
|
|
11
|
+
assembler = Assembler.new(ast, context.component_id)
|
|
12
|
+
assembler.assemble
|
|
13
|
+
Renderer.new(assembler, context)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CompEx
|
|
4
|
+
class UnsafeString
|
|
5
|
+
attr_reader :raw, :escaped
|
|
6
|
+
|
|
7
|
+
def initialize(value)
|
|
8
|
+
if value.is_a? UnsafeString
|
|
9
|
+
@raw = value.raw
|
|
10
|
+
@escaped = value.escaped
|
|
11
|
+
else
|
|
12
|
+
@raw = value
|
|
13
|
+
@escaped = ERB::Util.html_escape(raw)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s = @escaped
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/compex.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha1"
|
|
4
|
+
require "digest/sha2"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
require "nokogiri"
|
|
8
|
+
require "minicss"
|
|
9
|
+
require "minihtml"
|
|
10
|
+
|
|
11
|
+
require_relative "compex/version"
|
|
12
|
+
require_relative "compex/template"
|
|
13
|
+
require_relative "compex/string_refinements"
|
|
14
|
+
require_relative "compex/cache"
|
|
15
|
+
require_relative "compex/style"
|
|
16
|
+
require_relative "compex/bag"
|
|
17
|
+
require_relative "compex/js_rewriter"
|
|
18
|
+
require_relative "compex/component_registry"
|
|
19
|
+
require_relative "compex/base"
|
|
20
|
+
require_relative "compex/unsafe_string"
|
|
21
|
+
require_relative "compex/config"
|
|
22
|
+
require_relative "compex/asset_provider"
|
|
23
|
+
|
|
24
|
+
module CompEx
|
|
25
|
+
class Error < StandardError; end
|
|
26
|
+
|
|
27
|
+
class ParsingError < Error
|
|
28
|
+
attr_reader :errors
|
|
29
|
+
|
|
30
|
+
def initialize(errors)
|
|
31
|
+
@errors = errors
|
|
32
|
+
super("Errors were detected parsing the template: #{@errors.map { it[:error] }.join(", ")}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class MultipleRootError < Error; end
|
|
37
|
+
class NonElementRootError < Error; end
|
|
38
|
+
end
|