Module: Ruwi::Template::BuildVdom

Defined in:
lib/ruwi/runtime/template/build_vdom.rb

Class Method Summary collapse

Class Method Details

.build(elements) ⇒ String

Parameters:

  • elements (JS.Array)

Returns:

  • (String)


10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 10

def build(elements)
  vdom = []
  i = 0
  elements_length = elements[:length].to_i

  while i < elements_length
    element = elements[i]

    # text node
    if element[:nodeType] == JS.global[:Node][:TEXT_NODE]
      text_result = parse_text_node(element)
      vdom << text_result if text_result
      i += 1
      next
    end

    tag_name = element[:tagName].to_s.downcase

    # fragment node (including div elements with data-template attribute)
    if element[:nodeType] == JS.global[:Node][:ELEMENT_NODE] && (tag_name == 'template' || (tag_name == 'div' && has_data_template_attribute?(element)))
      # Check for conditional attributes on template
      if Ruwi::Template::BuildConditionalGroup.has_conditional_attribute?(element)
        # Process conditional group (r-if, r-elsif, r-else)
        conditional_group, next_index = Ruwi::Template::BuildConditionalGroup.build_conditional_group(elements, i)
        vdom << conditional_group
        i = next_index
      else
        vdom << build_fragment(element, tag_name)
        i += 1
      end
      next
    end

    # element node (including components)
    if element[:nodeType] == JS.global[:Node][:ELEMENT_NODE]
      # Check for conditional attributes on all elements (including components)
      if Ruwi::Template::BuildConditionalGroup.has_conditional_attribute?(element)
        # Process conditional group (r-if, r-elsif, r-else)
        conditional_group, next_index = Ruwi::Template::BuildConditionalGroup.build_conditional_group(elements, i)
        vdom << conditional_group
        i = next_index
      # Check for r-for attribute on all elements (including components)
      elsif Ruwi::Template::BuildForGroup.has_for_attribute?(element)
        # Process r-for loop - the result is a map expression that returns an array
        for_loop = Ruwi::Template::BuildForGroup.build_for_loop(element)
        if for_loop && !for_loop.empty?
          # Wrap the map result with splat operator to expand the array
          vdom << "*#{for_loop}"
        end
        i += 1
      else
        # Handle components and regular elements
        if is_component?(tag_name)
          vdom << build_component(element, tag_name)
        else
          vdom << build_element(element, tag_name)
        end
        i += 1
      end
      next
    end

    i += 1
  end

  vdom.compact.join(',')
end

.build_component(element, tag_name, filtered_attributes = nil) ⇒ String

Build component element

Parameters:

  • element (JS.Object)
  • tag_name (String)
  • filtered_attributes (Array) (defaults to: nil)

Returns:

  • (String)


198
199
200
201
202
203
204
205
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 198

def build_component(element, tag_name, filtered_attributes = nil)
  attributes_str = parse_attributes(filtered_attributes || element[:attributes].to_a)
  children = build(element[:childNodes])

  # Convert kebab-case to PascalCase for component name
  component_name = tag_name.split('-').map(&:capitalize).join
  "Ruwi::Vdom.h(#{component_name}, {#{attributes_str}}, [#{children}])"
end

.build_element(element, tag_name, filtered_attributes = nil) ⇒ String

Build regular HTML element

Parameters:

  • element (JS.Object)
  • tag_name (String)
  • filtered_attributes (Array) (defaults to: nil)

Returns:

  • (String)


212
213
214
215
216
217
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 212

def build_element(element, tag_name, filtered_attributes = nil)
  attributes_str = parse_attributes(filtered_attributes || element[:attributes].to_a)
  children = build(element[:childNodes])

  "Ruwi::Vdom.h('#{tag_name}', {#{attributes_str}}, [#{children}])"
end

.build_fragment(element, tag_name) ⇒ String

Build fragment or div element with data-template attribute

Parameters:

  • element (JS.Object)
  • tag_name (String)

Returns:

  • (String)


182
183
184
185
186
187
188
189
190
191
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 182

def build_fragment(element, tag_name)
  # div elements with data-template don't have content property, use childNodes directly
  if tag_name == 'template' && element[:content]
    content_nodes = element[:content][:childNodes]
  else
    content_nodes = element[:childNodes]
  end
  children = build(content_nodes)
  "Ruwi::Vdom.h_fragment([#{children}])"
end

.embed_script?(doc) ⇒ Boolean

Parameters:

  • doc (String)

Returns:

  • (Boolean)


151
152
153
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 151

def embed_script?(doc)
  doc.match?(/\{.+\}/)
end

.get_embed_script(script) ⇒ String

get value from embed script ex) Count: :count -> Count: component.state

Parameters:

  • script (String)

Returns:

  • (String)


159
160
161
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 159

def get_embed_script(script)
  script.gsub(/\{(.+)\}/) { ::Regexp.last_match(1) }
end

.has_data_template_attribute?(element) ⇒ Boolean

Check if element has data-template attribute

Parameters:

  • element (JS.Object)

Returns:

  • (Boolean)


166
167
168
169
170
171
172
173
174
175
176
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 166

def has_data_template_attribute?(element)
  return false unless element[:attributes]

  length = element[:attributes][:length].to_i
  length.times do |i|
    attribute = element[:attributes][i]
    key = attribute[:name].to_s
    return true if key == 'data-template'
  end
  false
end

.is_component?(tag_name) ⇒ Boolean

Parameters:

  • tag_name (String)

Returns:

  • (Boolean)


141
142
143
144
145
146
147
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 141

def is_component?(tag_name)
  # Component tags start with letter but exclude standard HTML elements
  return false unless tag_name.match?(/^[a-z]/)

  # Use the standard HTML elements list from Parser module
  !Ruwi::Template::Parser::STANDARD_HTML_ELEMENTS.include?(tag_name)
end

.parse_attributes(attributes) ⇒ String

Parse attributes array

Parameters:

  • attributes (Array)

Returns:

  • (String)


117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 117

def parse_attributes(attributes)
  attributes_str = []

  attributes.each do |attribute|
    key = attribute[:name].to_s
    value = attribute[:value].to_s

    if embed_script?(value)
      # Special handling for 'on' attribute to preserve hash structure
      if key == 'on'
        attributes_str << ":#{key} => #{value}"
      else
        attributes_str << ":#{key} => #{get_embed_script(value)}"
      end
      next
    end

    attributes_str << ":#{key} => '#{value}'"
  end
  attributes_str.join(', ')
end

.parse_text_node(element) ⇒ String

parse text node ex) “test” -> “test” ex) “test :count” -> “test #:count” ex) “test + 1” -> “test #+ 1” ex) “test :count test” -> “test #:count test” ex) “test :count test :count test” -> “test #:count test #:count test”

Parameters:

  • element (JS.Object)

Returns:

  • (String)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ruwi/runtime/template/build_vdom.rb', line 86

def parse_text_node(element)
  value = element[:nodeValue].to_s.chomp.strip

  return nil if value.empty?

  # Split the text by embedded script pattern and process each part
  # Regular expression explanation:
  # (        : Start capture group (this ensures the pattern itself is included in the result)
  #  \{      : Match an opening curly brace (escaped because { is special in regex)
  #  [^}]+   : Match one or more characters that are not a closing curly brace
  #  \}      : Match a closing curly brace (escaped because } is special in regex)
  # )        : End capture group
  # Example:
  #   Input:  "hello {state[:count]} world"
  #   Output: ["hello ", "{state[:count]}", " world"]
  parts = value.split(/(\{[^}]+\})/)
  processed_parts = parts.map do |part|
    if embed_script?(part)
      "\#{#{get_embed_script(part)}}"
    else
      part
    end
  end

  # Join all parts and wrap in double quotes
  %("#{processed_parts.join}")
end