Module: Ruwi::Template::Parser

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

Constant Summary collapse

STANDARD_HTML_ELEMENTS =

Standard HTML elements that should not be treated as custom components

%w[
  a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup
  data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6
  head header hr html i iframe img input ins kbd label legend li link main map mark meta meter nav noscript object ol
  optgroup option output p param picture pre progress q rp rt ruby s samp script section select small source span
  strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var video wbr
].freeze

Class Method Summary collapse

Class Method Details

.parse(template) ⇒ String

Parameters:

  • template (String)

Returns:

  • (String)


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/ruwi/runtime/template/parser.rb', line 33

def parse(template)
  # Replace <template> with <div data-template> to work around DOMParser limitations
  processed_template = preprocess_template_tag(template)

  # Convert PascalCase component names to kebab-case
  processed_template = preprocess_pascal_case_component_name(processed_template)

  # Preprocess self-closing custom element tags
  processed_template = preprocess_self_closing_tags(processed_template)

  parser = JS.eval('return new DOMParser()')
  document = parser.call(:parseFromString, JS.try_convert(processed_template), 'text/html')
  elements = document.getElementsByTagName('body')[0][:childNodes]

  Ruwi::Template::BuildVdom.build(elements)
end

.parse_and_eval(template, binding) ⇒ Ruwi::Vdom

Parameters:

  • template (String)
  • binding (Binding)

Returns:



20
21
22
23
24
25
26
27
28
29
# File 'lib/ruwi/runtime/template/parser.rb', line 20

def parse_and_eval(template, binding)
  vdom_code = parse(template)

  # If the code contains multiple top-level expressions, wrap them in a fragment
  if vdom_code.include?('end,') || (vdom_code.count(',') > 0 && !vdom_code.start_with?('['))
    vdom_code = "Ruwi::Vdom.h_fragment([#{vdom_code}])"
  end

  eval(vdom_code, binding)
end

.preprocess_pascal_case_component_name(template) ⇒ String

Convert PascalCase component names to kebab-case in template

Parameters:

  • template (String)

Returns:

  • (String)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/ruwi/runtime/template/parser.rb', line 53

def preprocess_pascal_case_component_name(template)
  processed_template = template.dup

  # Convert opening tags (e.g., <ButtonComponent> -> <button-component>)
  # Pattern explanation:
  # - <: Matches the opening angle bracket
  # - ([A-Z][a-zA-Z0-9]*): Captures PascalCase component name
  #   - [A-Z]: First letter must be uppercase
  #   - [a-zA-Z0-9]*: Followed by any number of letters or numbers
  # - (\s|>|\/): Captures the delimiter after the component name
  #   - \s: Whitespace for attributes
  #   - >: End of opening tag
  #   - \/: Self-closing tag
  # - /i: Case-insensitive matching
  processed_template = processed_template.gsub(/<([A-Z][a-zA-Z0-9]*)(\s|>|\/)/i) do
    component_name = ::Regexp.last_match(1)  # e.g., "ButtonComponent"
    delimiter = ::Regexp.last_match(2)       # e.g., " " or ">" or "/"

    # Convert component name to kebab-case:
    # 1. Insert hyphen before capital letters: ButtonComponent -> Button-Component
    # 2. Convert to lowercase: Button-Component -> button-component
    kebab_name = component_name.gsub(/([a-z0-9])([A-Z])/, '\1-\2').downcase

    "<#{kebab_name}#{delimiter}"
  end

  # Convert closing tags (e.g., </ButtonComponent> -> </button-component>)
  # Pattern explanation:
  # - <\/: Matches the closing tag prefix
  # - ([A-Z][a-zA-Z0-9]*): Captures PascalCase component name (same as above)
  # - >: Matches the closing angle bracket
  # - /i: Case-insensitive matching
  processed_template = processed_template.gsub(/<\/([A-Z][a-zA-Z0-9]*)>/i) do
    component_name = ::Regexp.last_match(1)  # e.g., "ButtonComponent"

    # Convert component name to kebab-case (same process as above)
    kebab_name = component_name.gsub(/([a-z0-9])([A-Z])/, '\1-\2').downcase

    "</#{kebab_name}>"
  end

  processed_template
end

.preprocess_self_closing_tags(template) ⇒ String

Convert self-closing custom element tags to regular tags Custom elements are identified by having hyphens in their name Standard void elements (img, input, etc.) are not converted

Parameters:

  • template (String)

Returns:

  • (String)


120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/ruwi/runtime/template/parser.rb', line 120

def preprocess_self_closing_tags(template)
  # Pattern matches: <tag-name attributes />
  # Where tag-name contains at least one hyphen (custom element convention)
  # Use a more robust pattern that handles nested brackets and quotes
  template.gsub(/<([a-z]+(?:-[a-z]+)+)((?:[^>]|"[^"]*"|'[^']*')*?)\/>/i) do
    tag_name = ::Regexp.last_match(1)
    attributes = ::Regexp.last_match(2)

    # Convert to regular open/close tags
    "<#{tag_name}#{attributes}></#{tag_name}>"
  end
end

.preprocess_template_tag(template) ⇒ String

Replace <template> tags with <div data-template> to work around DOMParser limitations

Parameters:

  • template (String)

Returns:

  • (String)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/ruwi/runtime/template/parser.rb', line 100

def preprocess_template_tag(template)
  processed_template = template.dup

  # Replace <template> with attributes (e.g., <template class="container">)
  processed_template = processed_template.gsub(/<template\s/, '<div data-template ')

  # Replace simple <template> without attributes
  processed_template = processed_template.gsub(/<template>/, '<div data-template>')

  # Replace closing tag
  processed_template = processed_template.gsub(/<\/template>/, '</div>')

  processed_template
end