├── .rspec ├── lib ├── webface_haml.rb └── webface_haml │ └── parser.rb ├── Gemfile ├── spec ├── spec_helper.rb └── lib │ └── webface_haml │ └── webface_parser_spec.rb ├── webface_haml.gemspec ├── example.haml └── Gemfile.lock /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /lib/webface_haml.rb: -------------------------------------------------------------------------------- 1 | module WebfaceHaml;end 2 | 3 | require 'haml' 4 | require_relative 'webface_haml/parser' 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "haml", git: "https://github.com/haml/haml" 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rspec" 8 | end 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | require File.dirname(__FILE__) + "/../lib/webface_haml.rb" 4 | 5 | RSpec.configure do |config| 6 | end 7 | -------------------------------------------------------------------------------- /webface_haml.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "webface_haml" 3 | s.version = "0.0.1" 4 | s.date = "2017-04-11" 5 | s.summary = "A haml extension to generate Webface components" 6 | s.description = "A haml extension to generate Webface components" 7 | s.authors = ["Roman Snitko"] 8 | s.email = "roman.snitko@gmail.com" 9 | s.files = [ 10 | "lib/webface_haml.rb", 11 | "spec/webface_haml.rb", 12 | "spec/spec_helper.rb", 13 | "example.haml", 14 | "README", 15 | ".rspec", 16 | "Gemfile.lock", 17 | "Gemfile" 18 | ] 19 | s.homepage = "https://github.com/snitko/webface_haml" 20 | s.license = "MIT" 21 | end 22 | -------------------------------------------------------------------------------- /example.haml: -------------------------------------------------------------------------------- 1 | // General components syntax: 2 | %%component_name (role1,role2) (property1,property2) { attr_property_with_value: "value", attr_property_with_diff_name(name), attr_prop3(prop3): "value"} %tag_name 3 | // 4 | // For components, both formats of specifying names (snake_case an CamelCase) are acceptable: 5 | %%component_name 6 | %%ComponentName 7 | // or only set the ones we want: 8 | %%component_name(role1,role2){ property1: "value1" } 9 | // or 10 | %%component_name{ property2: "value2" } 11 | // 12 | // Sometimes we need to have a different html attribute name for the value 13 | %%component{ property1(prop1): 'value' } 14 | // 15 | // or the same without specifying value 16 | %%component{ property1(prop1) } 17 | // 18 | // parts syntax: 19 | %%:part_name 20 | %%:part_name%tag_name 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/haml/haml 3 | revision: a8bb9e085fda7c8b41eacf96a7fcd5df9d845f6d 4 | specs: 5 | haml (5.0.4) 6 | temple (>= 0.8.0) 7 | tilt 8 | 9 | GEM 10 | remote: http://rubygems.org/ 11 | specs: 12 | diff-lcs (1.3) 13 | rspec (3.8.0) 14 | rspec-core (~> 3.8.0) 15 | rspec-expectations (~> 3.8.0) 16 | rspec-mocks (~> 3.8.0) 17 | rspec-core (3.8.0) 18 | rspec-support (~> 3.8.0) 19 | rspec-expectations (3.8.2) 20 | diff-lcs (>= 1.2.0, < 2.0) 21 | rspec-support (~> 3.8.0) 22 | rspec-mocks (3.8.0) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.8.0) 25 | rspec-support (3.8.0) 26 | temple (0.8.0) 27 | tilt (2.0.9) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | bundler 34 | haml! 35 | rspec 36 | 37 | BUNDLED WITH 38 | 1.16.1 39 | -------------------------------------------------------------------------------- /spec/lib/webface_haml/webface_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe WebfaceHaml::Parser do 4 | 5 | def render(text) 6 | Haml::Engine.new(text, parser_class: WebfaceHaml::Parser).render.rstrip 7 | end 8 | 9 | context "parser methods" do 10 | 11 | before(:each) do 12 | @parser = WebfaceHaml::Parser.new([]) 13 | end 14 | 15 | it "parses the component part of the haml line" do 16 | result = @parser.component_part_parsed("%%form_field(country_selector,selector)") 17 | expect(result).to eq({ class: "FormFieldComponent", roles: "country_selector,selector" }) 18 | end 19 | 20 | it "inserts roles into the component-related attributes in the original haml" do 21 | result = @parser.add_component_data_to_tag({ class: "FormFieldComponent", roles: "country_selector,selector" }, '%div(align="left")') 22 | expect(result).to eq('%div(align="left" data-component-class="FormFieldComponent" data-component-roles="country_selector,selector"){}') 23 | end 24 | 25 | it "inserts component attribute properties and their values into the component-related attributes in the original haml" do 26 | result = @parser.add_component_data_to_tag({ class: "FormFieldComponent", attribute_properties: 'hello: "world"'}, '%div(align="left")') 27 | expect(result).to eq('%div(align="left" data-component-class="FormFieldComponent" data-hello="world" data-component-attribute-properties="hello:data-hello"){}') 28 | end 29 | 30 | it "inserts component part attributes into the component-related attributes in the original haml" do 31 | result = @parser.add_component_data_to_tag({ part: "input_field" }, '%input') 32 | expect(result).to eq('%input(data-component-part="input_field"){}') 33 | end 34 | 35 | end 36 | 37 | it "adds component name as data attribute to the tag" do 38 | expect(render("%%DialogWindow%div hello")).to eq("
hello
") 39 | end 40 | 41 | it "adds part name as data attribute to the tag" do 42 | expect(render("%%:input_field%input")).to eq("") 43 | end 44 | 45 | it "adds roles as data attributes to the tag" do 46 | expect(render("%%:input_field(role1,role2)%input")).to eq("") 47 | end 48 | 49 | it "adds property data attribute to the tag" do 50 | expect(render("%%:input_field(role1,role2)(property1)%.input")).to eq("
") 51 | end 52 | 53 | it "adds attribute properties data attribute to the component tag" do 54 | expect(render("%%button{ my_value: 'click' }%div")).to eq("
") 55 | end 56 | 57 | it "adds attribute properties that have a different name as html attributes" do 58 | expect(render("%%button{ value(data-val): 'click' }%div")).to eq("
") 59 | end 60 | 61 | it "adds attribute into attribute properties names, but not values" do 62 | expect(render("%%button{ value(data-val) }%div")).to eq("
") 63 | end 64 | 65 | it "allows to skip tag name" do 66 | expect(render("%%button%.button hello")).to eq("
hello
") 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /lib/webface_haml/parser.rb: -------------------------------------------------------------------------------- 1 | module WebfaceHaml 2 | class Parser < ::Haml::Parser 3 | 4 | def process_line(haml_line) 5 | component_part_string = haml_line.text.match(/\A%%.*?%/) 6 | if component_part_string 7 | component_part_string = component_part_string[0] 8 | haml_line.text = haml_line.text.sub(/\A%%.*?%/, "%") 9 | haml_line.text = add_component_data_to_tag(component_part_parsed(component_part_string), haml_line.text) 10 | end 11 | 12 | super(haml_line) 13 | end 14 | 15 | # Parses component_part string, creating a hash with 16 | # each Component logical element content and a method to be called 17 | # to insert this content into the tag. 18 | # 19 | # Input.: component_part_string = "form_field(country_selector,selector)" 20 | # Result: { class: "FormField", roles: "country_selector,selector" } 21 | def component_part_parsed(string) 22 | matches = string.chomp("%").match(/%%([:a-zA-Z0-9_]+)?\s*(\(.*?\))?\s*(\(.*?\))?\s*(\{.*\})?/) 23 | 24 | parsed = {} 25 | 26 | if matches[1].start_with?(":") 27 | parsed[:part] = matches[1].sub(/\A:/, "") 28 | elsif !matches[1].nil? 29 | if(matches[1].to_s.include?("_") || matches[1][0] != matches[1][0].upcase) 30 | parsed[:class] = matches[1].split('_').collect(&:capitalize).join + "Component" 31 | else 32 | parsed[:class] = matches[1] + "Component" 33 | end 34 | end 35 | 36 | parsed[:roles] = matches[2].gsub(/[\(\)]/, "") if matches[2] 37 | parsed[:property] = matches[3].gsub(/[\(\)]/, "") if matches[3] 38 | parsed[:attribute_properties] = matches[4].gsub(/[\{\}]/, "") if matches[4] 39 | 40 | parsed 41 | end 42 | 43 | # Modifies the original haml tag adding necessary data- attributes to indicate 44 | # this is a component. 45 | # 46 | # Input: component_part_parsed = "form_field(country_selector,selector)" 47 | # haml_line = "%input(name="country") 48 | # 49 | # Result: "%input(name="country" 50 | # data-component-class="FormFieldComponent" 51 | # data-component-roles="country_selector,selector") 52 | def add_component_data_to_tag(component_part_parsed, haml_text) 53 | 54 | # Parsing haml so we can later reconstruct it and insert our own 55 | # attributes. 56 | matches = haml_text.match(/%([a-zA-Z0-9_#.\-]+)?\s*(\(.+\))?\s*(\{.+\})?(.*)/) 57 | tag = matches[1] || "div" 58 | tag = "div#{tag}" if tag.match(/\A[.#]/) 59 | attrs_plain = matches[2] ? matches[2].gsub(/[\(\)]/, "") : "" 60 | attrs_curly = matches[3] ? matches[3].gsub(/[\{\}]/, "") : "" 61 | remainder = matches[4] 62 | 63 | component_part_parsed.each do |attr_name,value| 64 | 65 | # Turns attribute_properties passed into appropriate data- attributes. 66 | # Example: 67 | # Passed: { attr_1: "value1", attr_2: "value2" } 68 | # 69 | # Result (inside a tag) : data-component-property-attributes="attr_1:data-attr-1,attr_2:data-attr-2" 70 | # data-attr-1="value1" 71 | # data-attr-2="value2" 72 | if attr_name == :attribute_properties 73 | attribute_properties = [] 74 | parse_attribute_properties_hash(value).each do |k,v| 75 | if(k =~ /\A.+\(.*\)\Z/) 76 | _attrs = k.split('(') 77 | webface_attr_name = _attrs[0] 78 | html_attr_name = _attrs[1].chomp(")") 79 | else 80 | webface_attr_name = k 81 | html_attr_name = "data-#{k}" 82 | end 83 | html_attr_name.gsub!("_", "-") 84 | attrs_plain += " #{html_attr_name}=\"#{v}\"" if v 85 | attribute_properties << "#{webface_attr_name}:#{html_attr_name}" 86 | end 87 | attrs_plain += " data-component-#{attr_name.to_s.gsub("_", "-")}=\"#{attribute_properties.join(",")}\"" 88 | 89 | # Take care of the rest of component data- attributes, such as 90 | # data-component-class, data-component-roles and data-component-property 91 | else 92 | attrs_plain += " data-component-#{attr_name.to_s.gsub("_", "-")}=\"#{value}\"" 93 | end 94 | end 95 | 96 | "%#{tag}(#{attrs_plain.lstrip}){#{attrs_curly}} #{remainder}".rstrip 97 | end 98 | 99 | # Attribute properties are passed as in a format that mirrors Ruby Hash. Example: 100 | # { attribute1: "value1", attribute2: "value2" }. 101 | # This method parses the string and turns it into an actual Ruby Hash. 102 | def parse_attribute_properties_hash(s) 103 | hash = {} 104 | s.split(",").map { |i| i.lstrip!; i.rstrip }.each do |i| 105 | k,v= i.split(/\s*:\s*/) 106 | # TODO this evaluates out of the views context, need to figure out a way to provide the right scope for eval 107 | hash[k] = v ? eval(v) : nil 108 | end 109 | hash 110 | end 111 | 112 | 113 | end 114 | end 115 | --------------------------------------------------------------------------------