Created
September 6, 2025 18:12
-
-
Save activestylus/7fb6acd539d3775c748713ef8d901c82 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ruby | |
| require 'cgi' | |
| require 'set' | |
| require 'benchmark' | |
| module Joys | |
| TAGS = %w[style div h1 h2 h3 h4 h5 h6 p span a img button html head title body meta link ul ol li nav header main footer section article form input textarea select option].freeze | |
| VOID_TAGS = %w[img meta link br hr input].freeze | |
| BOOLEAN_ATTRS = %w[disabled readonly multiple checked selected autofocus novalidate formnovalidate hidden required open reversed scoped seamless muted autoplay controls loop default inert itemscope].to_set.freeze | |
| @cache = {} | |
| @layouts = {} | |
| @components = {} | |
| @pages = {} | |
| @compiled_styles = {} | |
| @consolidated_cache = {} | |
| class << self | |
| attr_reader :cache, :layouts, :components, :pages, :compiled_styles, :consolidated_cache | |
| end | |
| def self.cache(cache_id, *args, &block) | |
| cache_key = [cache_id, args.hash] | |
| @cache[cache_key] ||= block.call | |
| end | |
| def self.clear_cache! | |
| @cache.clear | |
| end | |
| module Styles | |
| @compiled_styles = {} | |
| @consolidated_cache = {} | |
| class << self | |
| attr_reader :compiled_styles, :consolidated_cache | |
| end | |
| def self.compile_component_styles(comp_name, base_css, media_queries, scoped) | |
| scope_prefix = scoped ? ".#{comp_name.tr('_', '-')} " : "" | |
| processed_base_css = base_css.map { |css| scope_css(css, scope_prefix) } | |
| processed_media_queries = {} | |
| media_queries.each do |key, css_rules| | |
| scoped_rules = css_rules.map { |css| scope_css(css, scope_prefix) } | |
| processed_media_queries[key] = scoped_rules | |
| end | |
| @compiled_styles[comp_name] = { | |
| base_css: processed_base_css, | |
| media_queries: processed_media_queries | |
| }.freeze | |
| nil | |
| end | |
| def self.render_consolidated_styles(used_components) | |
| return "" if used_components.empty? | |
| cache_key = used_components.to_a.sort.join(',') | |
| return @consolidated_cache[cache_key] if @consolidated_cache.key?(cache_key) | |
| buffer = String.new(capacity: 4096) | |
| min_queries = Hash.new { |h,k| h[k] = [] } | |
| max_queries = Hash.new { |h,k| h[k] = [] } | |
| seen_base = Set.new | |
| used_components.each do |comp| | |
| styles = @compiled_styles[comp] || next | |
| styles[:base_css].each do |rule| | |
| next if seen_base.include?(rule) | |
| buffer << rule | |
| seen_base << rule | |
| end | |
| styles[:media_queries].each do |key, rules| | |
| case key | |
| when /^min-(\d+)$/ | |
| min_queries[$1.to_i].concat(rules) | |
| when /^max-(\d+)$/ | |
| max_queries[$1.to_i].concat(rules) | |
| end | |
| end | |
| end | |
| max_queries.sort.reverse.each do |bp,rules| | |
| buffer << "@media (max-width: #{bp}px){" | |
| rules.uniq.each { |r| buffer << r } | |
| buffer << "}" | |
| end | |
| min_queries.sort.each do |bp,rules| | |
| buffer << "@media (min-width: #{bp}px){" | |
| rules.uniq.each { |r| buffer << r } | |
| buffer << "}" | |
| end | |
| @consolidated_cache[cache_key] = buffer.freeze | |
| end | |
| def self.render_external_styles(used_components) | |
| "<link rel=\"stylesheet\" href=\"/css/compiled.css\">" | |
| end | |
| private | |
| def self.scope_css(css, scope_prefix) | |
| return css if scope_prefix.empty? | |
| scope_prefix + css | |
| end | |
| end | |
| class MockParams | |
| def initialize | |
| @accesses = Set.new | |
| end | |
| def [](key) | |
| @accesses << key | |
| MockValue.new(key) | |
| end | |
| attr_reader :accesses | |
| end | |
| class MockValue | |
| def initialize(param_name) | |
| @param_name = param_name | |
| end | |
| def [](key) | |
| MockValue.new("#{@param_name}[#{key.inspect}]") | |
| end | |
| def first | |
| MockValue.new("#{@param_name}.first") | |
| end | |
| def last | |
| MockValue.new("#{@param_name}.last") | |
| end | |
| def length; MockValue.new("#{@param_name}.length"); end | |
| def size; MockValue.new("#{@param_name}.size"); end | |
| def count(&block); MockValue.new("#{@param_name}.count"); end | |
| def empty?; MockValue.new("#{@param_name}.empty?"); end | |
| def join(separator = "") | |
| if separator == "" | |
| MockValue.new("#{@param_name}.join") | |
| else | |
| MockValue.new("#{@param_name}.join(QUOTE#{separator}QUOTE)") | |
| end | |
| end | |
| def keys; MockArray.new("#{@param_name}.keys"); end | |
| def values; MockArray.new("#{@param_name}.values"); end | |
| def key?(key); MockValue.new("#{@param_name}.key?(QUOTE#{key}QUOTE)"); end | |
| def split(delimiter = " ") | |
| MockArray.new("#{@param_name}.split(QUOTE#{delimiter}QUOTE)") | |
| end | |
| def strip; MockValue.new("#{@param_name}.strip"); end | |
| def upcase; MockValue.new("#{@param_name}.upcase"); end | |
| def downcase; MockValue.new("#{@param_name}.downcase"); end | |
| def select(&block) | |
| if block | |
| MockArray.new("#{@param_name}.select") | |
| else | |
| MockValue.new("#{@param_name}.select") | |
| end | |
| end | |
| def map(&block) | |
| if block | |
| MockArray.new("#{@param_name}.map") | |
| else | |
| MockValue.new("#{@param_name}.map") | |
| end | |
| end | |
| def each(&block) | |
| if block | |
| mock_item = MockItem.new | |
| yield(mock_item) | |
| end | |
| self | |
| end | |
| def to_s | |
| ":PARAM_#{@param_name}" | |
| end | |
| end | |
| class MockArray | |
| def initialize(expression) | |
| @expression = expression | |
| end | |
| def join(separator = "") | |
| if separator == "" | |
| MockValue.new("#{@expression}.join") | |
| else | |
| MockValue.new("#{@expression}.join(QUOTE#{separator}QUOTE)") | |
| end | |
| end | |
| def map(&block) | |
| if block | |
| MockArray.new("#{@expression}.map") | |
| else | |
| MockValue.new("#{@expression}.map") | |
| end | |
| end | |
| def select(&block) | |
| if block | |
| MockArray.new("#{@expression}.select") | |
| else | |
| MockValue.new("#{@expression}.select") | |
| end | |
| end | |
| def first; MockValue.new("#{@expression}.first"); end | |
| def last; MockValue.new("#{@expression}.last"); end | |
| def length; MockValue.new("#{@expression}.length"); end | |
| def size; MockValue.new("#{@expression}.size"); end | |
| def count; MockValue.new("#{@expression}.count"); end | |
| def empty?; MockValue.new("#{@expression}.empty?"); end | |
| def [](index) | |
| MockValue.new("#{@expression}[#{index.inspect}]") | |
| end | |
| def each(&block) | |
| if block | |
| mock_item = MockItem.new | |
| yield(mock_item) | |
| end | |
| self | |
| end | |
| def to_s | |
| ":PARAM_#{@expression}" | |
| end | |
| end | |
| class MockItem | |
| def [](key) | |
| ":ITEM_#{key}" | |
| end | |
| end | |
| class DSLCapturer | |
| def initialize | |
| @operations = [] | |
| @tag_stack = [] | |
| @params = MockParams.new | |
| end | |
| attr_reader :operations, :params | |
| def capture(&block) | |
| instance_eval(&block) | |
| @operations | |
| end | |
| TAGS.each do |tag| | |
| define_method(tag) do |content = nil, cs: nil, raw: false, **attrs, &block| | |
| if block | |
| @operations << [:tag_open, tag, cs, attrs] | |
| instance_eval(&block) | |
| @operations << [:tag_close, tag] | |
| else | |
| @operations << [:tag_complete, tag, content, cs, attrs, raw] | |
| end | |
| end | |
| end | |
| VOID_TAGS.each do |tag| | |
| define_method(tag) do |cs: nil, **attrs| | |
| @operations << [:void_tag, tag, cs, attrs] | |
| end | |
| end | |
| def txt(content) | |
| @operations << [:text, content, true] | |
| end | |
| def raw(content) | |
| @operations << [:text, content, false] | |
| end | |
| def comp(name, **kwargs) | |
| processed_kwargs = {} | |
| kwargs.each do |key, value| | |
| if value.respond_to?(:to_s) | |
| processed_kwargs[key] = value.to_s | |
| else | |
| processed_kwargs[key] = value | |
| end | |
| end | |
| @operations << [:component, name, processed_kwargs] | |
| end | |
| def layout(name, &block) | |
| @operations << [:layout_start, name] | |
| instance_eval(&block) if block | |
| @operations << [:layout_end] | |
| end | |
| def push(slot_name, &block) | |
| @operations << [:slot_start, slot_name] | |
| instance_eval(&block) if block | |
| @operations << [:slot_end] | |
| end | |
| def pull(slot_name = :main) | |
| @operations << [:slot_pull, slot_name] | |
| end | |
| def styles(scoped: false, &block) | |
| @operations << [:styles_start, scoped] | |
| @style_context = { scoped: scoped, base_css: [], media_queries: {} } | |
| instance_eval(&block) if block | |
| @operations << [:styles_end, @style_context[:base_css], @style_context[:media_queries], scoped] | |
| @style_context = nil | |
| end | |
| def css(rules) | |
| return unless @style_context | |
| rules = [rules] unless rules.is_a?(Array) | |
| @style_context[:base_css].concat(rules) | |
| end | |
| def min_media(breakpoint, rules) | |
| return unless @style_context | |
| key = "min-#{breakpoint}" | |
| @style_context[:media_queries][key] ||= [] | |
| @style_context[:media_queries][key] << rules | |
| end | |
| def max_media(breakpoint, rules) | |
| return unless @style_context | |
| key = "max-#{breakpoint}" | |
| @style_context[:media_queries][key] ||= [] | |
| @style_context[:media_queries][key] << rules | |
| end | |
| def minmax_media(min_bp, max_bp, rules) | |
| return unless @style_context | |
| key = "minmax-#{min_bp}-#{max_bp}" | |
| @style_context[:media_queries][key] ||= [] | |
| @style_context[:media_queries][key] << rules | |
| end | |
| def pull_styles | |
| @operations << [:pull_styles] | |
| end | |
| def pull_external_styles | |
| @operations << [:pull_external_styles] | |
| end | |
| def doctype | |
| @operations << [:doctype] | |
| end | |
| def method_missing(method_name, *args, &block) | |
| if block | |
| @operations << [:iteration_start, method_name] | |
| instance_eval(&block) | |
| @operations << [:iteration_end] | |
| else | |
| @operations << [:unknown_call, method_name, args] | |
| end | |
| end | |
| end | |
| class CodeGenerator | |
| def self.generate_layout(operations) | |
| html_content = generate_html_content(operations, []) | |
| <<~RUBY | |
| lambda do |**slots| | |
| <<~HTML | |
| #{indent_content(html_content)} | |
| HTML | |
| end | |
| RUBY | |
| end | |
| def self.generate_component(operations, param_accesses, comp_name) | |
| if param_accesses.any? | |
| param_sig = param_accesses.map { |p| "#{p}:" }.join(', ') + ", **kwargs" | |
| else | |
| param_sig = "**kwargs" | |
| end | |
| html_content = generate_html_content(operations, param_accesses, comp_name) | |
| <<~RUBY | |
| lambda do |#{param_sig}| | |
| <<~HTML | |
| #{indent_content(html_content)} | |
| HTML | |
| end | |
| RUBY | |
| end | |
| def self.generate_page(operations, param_accesses) | |
| if param_accesses.any? | |
| param_sig = param_accesses.map { |p| "#{p}:" }.join(', ') + ", **kwargs" | |
| else | |
| param_sig = "**kwargs" | |
| end | |
| layout_name, slots = parse_page_structure(operations, param_accesses) | |
| slot_code = slots.map do |slot_name, slot_ops| | |
| slot_html = generate_html_content(slot_ops, param_accesses) | |
| " #{slot_name}: (<<~HTML\n#{indent_content(slot_html, 6)}\n HTML\n )" | |
| end.join(",\n") | |
| layout_line = layout_name ? "layout: :#{layout_name}," : "" | |
| <<~RUBY | |
| lambda do |#{param_sig}| | |
| @used_components = Set.new | |
| result = { | |
| #{layout_line} | |
| #{slot_code} | |
| } | |
| result | |
| end | |
| RUBY | |
| end | |
| def self.generate_html_content(operations, param_accesses, comp_name = 'component') | |
| lines = [] | |
| # Find operations that contain :ITEM_ markers for iteration | |
| item_indices = Set.new | |
| operations.each_with_index do |op, i| | |
| if op.any? { |part| part.to_s.include?(":ITEM_") } | |
| item_indices << i | |
| end | |
| end | |
| # Process operations | |
| operations.each_with_index do |op, i| | |
| case op[0] | |
| when :tag_complete | |
| _, tag, content, cs, attrs, raw = op | |
| if item_indices.include?(i) | |
| # This is an iteration item - process the entire iteration block | |
| iteration_lines = [generate_complete_tag(tag, content, cs, attrs, raw, param_accesses)] | |
| collection_param = param_accesses.first || :items | |
| # Process iteration content to replace :ITEM_ placeholders | |
| processed_iteration_content = iteration_lines.map do |iter_line| | |
| iter_line.gsub(/:ITEM_(\w+)/) do |match| | |
| key = $1 | |
| "\#{CGI.escapeHTML(item[:#{key}].to_s)}" | |
| end | |
| end | |
| lines << "\#{#{collection_param}.map do |item|" | |
| lines << " <<~ITEM_HTML" | |
| processed_iteration_content.each { |ic| lines << " #{ic}" } | |
| lines << " ITEM_HTML" | |
| lines << "end.join}" | |
| else | |
| lines << generate_complete_tag(tag, content, cs, attrs, raw, param_accesses) | |
| end | |
| when :tag_open | |
| _, tag, cs, attrs = op | |
| lines << generate_opening_tag(tag, cs, attrs) | |
| when :tag_close | |
| _, tag = op | |
| lines << "</#{tag}>" | |
| when :text | |
| _, content, escaped = op | |
| lines << generate_text_content(content, escaped, param_accesses) | |
| when :component | |
| _, name, kwargs = op | |
| lines << generate_component_call(name, kwargs, param_accesses) | |
| when :slot_pull | |
| _, slot_name = op | |
| lines << "\#{slots[:#{slot_name}] || ''}" | |
| when :styles_end | |
| _, base_css, media_queries, scoped = op | |
| lines << "\#{Joys::Styles.compile_component_styles('#{comp_name}', #{base_css.inspect}, #{media_queries.inspect}, #{scoped}) && ''}" | |
| when :pull_styles | |
| lines << "\#{Joys::Styles.render_consolidated_styles(@used_components || Set.new)}" | |
| when :pull_external_styles | |
| lines << "\#{Joys::Styles.render_external_styles(@used_components || Set.new)}" | |
| end | |
| end | |
| lines.join("\n") | |
| end | |
| def self.generate_complete_tag(tag, content, cs, attrs, raw, param_accesses) | |
| attr_str = generate_attributes(cs, attrs) | |
| if content | |
| content_str = generate_text_content(content, !raw, param_accesses) | |
| "<#{tag}#{attr_str}>#{content_str}</#{tag}>" | |
| else | |
| "<#{tag}#{attr_str}></#{tag}>" | |
| end | |
| end | |
| def self.generate_opening_tag(tag, cs, attrs) | |
| attr_str = generate_attributes(cs, attrs) | |
| "<#{tag}#{attr_str}>" | |
| end | |
| def self.generate_attributes(cs, attrs) | |
| parts = [] | |
| if cs | |
| parts << " class=\"#{cs}\"" | |
| end | |
| attrs&.each do |k, v| | |
| next if v.nil? || v == false | |
| if v.is_a?(Hash) && (k.to_s == "data" || k.to_s.start_with?("aria")) | |
| v.each { |sk, sv| parts << " #{k}-#{sk.to_s.tr('_', '-')}=\"#{CGI.escapeHTML(sv.to_s)}\"" } | |
| elsif v == true && BOOLEAN_ATTRS.include?(k.to_s) | |
| parts << " #{k}" | |
| else | |
| parts << " #{k}=\"#{CGI.escapeHTML(v.to_s)}\"" | |
| end | |
| end | |
| parts.join | |
| end | |
| def self.generate_text_content(content, escaped, param_accesses) | |
| return "" unless content | |
| content_str = content.to_s | |
| # Step 1: Replace QUOTE markers with actual quotes | |
| content_str = content_str.gsub(/QUOTE(.*?)QUOTE/) { "\"#{$1}\"" } | |
| # Step 2: Handle complex parameter expressions with comprehensive regex | |
| # This regex captures method calls, array access, and combinations | |
| content_str = content_str.gsub(/:PARAM_([a-zA-Z_]\w*(?:(?:\[[^\]]*\]|\.[a-zA-Z_]\w*|\([^)]*\))*)*)/) do |match| | |
| expression = $1 | |
| base_param = expression.split(/[\.\[\(]/).first | |
| if param_accesses.include?(base_param.to_sym) | |
| escaped ? "\#{CGI.escapeHTML((#{expression}).to_s)}" : "\#{#{expression}}" | |
| else | |
| match | |
| end | |
| end | |
| content_str | |
| end | |
| def self.generate_component_call(name, kwargs, param_accesses) | |
| if kwargs.any? | |
| kwargs_str = kwargs.map do |k, v| | |
| v_str = v.to_s | |
| if v_str.start_with?(":ITEM_") | |
| key = v_str.sub(":ITEM_", "") | |
| "#{k}: item[:#{key}]" | |
| elsif v_str.start_with?(":PARAM_") | |
| param = v_str.sub(":PARAM_", "") | |
| "#{k}: #{param}" | |
| else | |
| "#{k}: #{v.inspect}" | |
| end | |
| end.join(", ") | |
| "\#{(@used_components ||= Set.new; @used_components << '#{name}'; Joys.comp(:#{name}, #{kwargs_str}))}" | |
| else | |
| "\#{Joys.comp(:#{name})}" | |
| end | |
| end | |
| def self.parse_page_structure(operations, param_accesses) | |
| layout_name = nil | |
| slots = {} | |
| current_slot = nil | |
| slot_operations = [] | |
| operations.each do |op| | |
| case op[0] | |
| when :layout_start | |
| layout_name = op[1] | |
| when :slot_start | |
| if current_slot | |
| slots[current_slot] = slot_operations.dup | |
| slot_operations.clear | |
| end | |
| current_slot = op[1].to_sym | |
| when :slot_end | |
| if current_slot | |
| slots[current_slot] = slot_operations.dup | |
| slot_operations.clear | |
| current_slot = nil | |
| end | |
| else | |
| slot_operations << op if current_slot | |
| end | |
| end | |
| [layout_name, slots] | |
| end | |
| def self.indent_content(content, spaces = 4) | |
| content.split("\n").map { |line| | |
| line.empty? ? "" : "#{' ' * spaces}#{line}" | |
| }.join("\n") | |
| end | |
| end | |
| def self.register(type, name, &template) | |
| begin | |
| capturer = DSLCapturer.new | |
| operations = capturer.capture(&template) | |
| param_accesses = capturer.params.accesses | |
| style_ops = operations.select { |op| op[0] == :styles_end } | |
| style_ops.each do |op| | |
| _, base_css, media_queries, scoped = op | |
| Joys::Styles.compile_component_styles(name.to_s, base_css, media_queries, scoped) | |
| end | |
| case type | |
| when :comp | |
| code = CodeGenerator.generate_component(operations, param_accesses, name) | |
| @components[name] = eval(code) | |
| when :layout | |
| code = CodeGenerator.generate_layout(operations) | |
| @layouts[name] = eval(code) | |
| when :page | |
| code = CodeGenerator.generate_page(operations, param_accesses) | |
| @pages[name] = eval(code) | |
| end | |
| rescue => e | |
| raise "Registration failed for #{type}:#{name} - #{e.message}" | |
| end | |
| end | |
| def self.comp(name, **kwargs) | |
| comp_func = @components[name] | |
| return "Component :#{name} not found" unless comp_func | |
| cache("comp_#{name}", kwargs.hash) { comp_func.call(**kwargs) } | |
| end | |
| def self.page(name, **locals) | |
| page_func = @pages[name] | |
| return "Page :#{name} not found" unless page_func | |
| result = cache("page_#{name}", locals.hash) do | |
| page_result = page_func.call(**locals) | |
| if page_result.is_a?(Hash) && page_result[:layout] | |
| layout_name = page_result.delete(:layout) | |
| layout_func = @layouts[layout_name] | |
| layout_func ? layout_func.call(**page_result) : "Layout not found" | |
| else | |
| page_result.to_s | |
| end | |
| end | |
| result.freeze | |
| end | |
| def self.html(&block) | |
| context = Object.new | |
| def context.comp(name, **kwargs) | |
| comp_func = Joys.components[name] | |
| return "Component :#{name} not found" unless comp_func | |
| result = comp_func.call(**kwargs) | |
| @bf << result if @bf | |
| nil | |
| end | |
| TAGS.each do |tag| | |
| context.define_singleton_method(tag) do |content = nil, cs: nil, raw: false, **attrs, &block| | |
| if block | |
| attr_str = build_tag_attributes(cs, attrs) | |
| @bf << "<#{tag}#{attr_str}>" | |
| instance_eval(&block) | |
| @bf << "</#{tag}>" | |
| else | |
| attr_str = build_tag_attributes(cs, attrs) | |
| content_html = raw ? content.to_s : CGI.escapeHTML(content.to_s) | |
| @bf << "<#{tag}#{attr_str}>#{content_html}</#{tag}>" | |
| end | |
| end | |
| end | |
| def context.build_tag_attributes(cs, attrs) | |
| parts = [] | |
| parts << " class=\"#{cs}\"" if cs | |
| attrs&.each do |k, v| | |
| next if v.nil? || v == false | |
| if v == true && BOOLEAN_ATTRS.include?(k.to_s) | |
| parts << " #{k}" | |
| else | |
| parts << " #{k}=\"#{CGI.escapeHTML(v.to_s)}\"" | |
| end | |
| end | |
| parts.join | |
| end | |
| context.instance_variable_set(:@bf, String.new(capacity: 8192)) | |
| context.instance_eval(&block) | |
| context.instance_variable_get(:@bf).freeze | |
| end | |
| end | |
| # Test framework | |
| Joys.register(:comp, :greeting) do | |
| div(cs: "greeting") do | |
| h1("Hello #{params[:name]}") | |
| p("Welcome!") | |
| end | |
| end | |
| Joys.register(:comp, :user_card) do | |
| styles do | |
| css ".card { background: #f9f9f9; padding: 1rem; margin: 0.5rem; border-radius: 4px; }" | |
| css ".card h3 { color: #333; margin: 0 0 0.5rem 0; }" | |
| css ".card p { color: #666; margin: 0; }" | |
| end | |
| div(cs: "card") do | |
| h3(params[:name]) | |
| p("Email: #{params[:email]}") | |
| end | |
| end | |
| Joys.register(:layout, :basic) do | |
| html do | |
| head do | |
| title { pull(:title) } | |
| style {pull_styles} | |
| end | |
| body do | |
| pull(:content) | |
| end | |
| end | |
| end | |
| Joys.register(:page, :users) do | |
| styles do | |
| css "body {color:red}" | |
| end | |
| layout(:basic) do | |
| push(:title) do | |
| txt("User List") | |
| end | |
| push(:content) do | |
| h1("Our Users") | |
| params[:users].each do |user| | |
| comp(:user_card, name: user[:name], email: user[:email]) | |
| end | |
| end | |
| end | |
| end | |
| Joys.register(:comp, :advanced_demo) do | |
| div do | |
| h3("User Stats") | |
| p("Total users: #{params[:users].length}") | |
| p("First user: #{params[:users].first[:name]}") | |
| p("Last user: #{params[:users].last[:name]}") | |
| p("Second user: #{params[:users][1][:name]}") | |
| h3("Metadata") | |
| p("Keys: #{params[:metadata].keys.join(', ')}") | |
| h3("Tags") | |
| p("Uppercase: #{params[:title].upcase}") | |
| h3("User List") | |
| ul do | |
| params[:users].each do |user| | |
| li("#{user[:name]} - #{user[:active] ? 'Active' : 'Inactive'}") | |
| end | |
| end | |
| end | |
| end | |
| test_users = [ | |
| { name: "Alice", email: "[email protected]", active: false }, | |
| { name: "Bob", email: "[email protected]", active: true } | |
| ] | |
| puts "=== Joys Framework Test ===" | |
| puts "\nComponent test:" | |
| puts Joys.comp(:greeting, name: "World") | |
| puts "\nUser card test:" | |
| puts Joys.comp(:user_card, name: "John", email: "[email protected]") | |
| puts "\nPage test:" | |
| puts Joys.page(:users, users: test_users) | |
| puts "\nAdvanced demo test:" | |
| puts Joys.comp(:advanced_demo, | |
| users: test_users, | |
| tags: ['hot','cold'], | |
| title: "Yo", | |
| metadata: { title: "hello", what: "that" }) | |
| puts "\nPerformance test:" | |
| n = 1000 | |
| time = Benchmark.realtime { n.times { Joys.comp(:greeting, name: "Test") } } | |
| puts "#{n} renders: #{(time * 1000 / n).round(3)}ms per render" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment