HTMLForm.generate(STDOUT) do comment "This is a simple HTML form" form :name => "registration", :action => "http://www.example.com/register.cgi" do content "Name:" br input :name => "name" br content "Post:" br textarea :name => "address", :rows=>6, :cols=>40 do "Please enter your post here" # Este bloque retorna contenido end br button { "Submit" } # Este bloque retorna contenido end end
[xmlgenerator]$ ruby xml_generator.rb > ejemplo.html
[xmlgenerator]$ cat ejemplo.html <!-- This is a simple HTML form --> <form name='registration' action='http://www.example.com/register.cgi' method='GET' enctype='application/x-www-form-urlencoded'>Name:<br/> <input name='name' type='text'/> <br/> Post:<br/> <textarea name='address' rows='6' cols='40'>Please enter your post here</textarea> <br/> <button type='submit'>Submit</button> </form>
Este DSL permite describir una gramática XML:
Que tags son permitidos y para cada tag que atributos son legales y de que tipo son. Se usaría así:
class HTMLForm < XMLGrammar element :form, :action => REQ, # atributo requerido :method => "GET", # cadena: valor por defecto :enctype => "application/x-www-form-urlencoded", :name => OPT # opcional element :input, :type => "text", :name => OPT, :value => OPT, :maxlength => OPT, :size => OPT, :src => OPT, :checked => BOOL, :disabled => BOOL, :readonly => BOOL element :textarea, :rows => REQ, :cols => REQ, :name => OPT, :disabled => BOOL, :readonly => BOOL element :button, :name => OPT, :value => OPT, :type => "submit", :disabled => OPT element :br end
El método element
es proveído por
XMLGrammar
y
construye un método de instancia
con el nombre especificado (por ejemplo, :form
) como primer
argumento en la subclase (HTMLForm
en el ejemplo).
Como segundo argumento opcional recibe un hash especificando los atributos
legales del elemento y de que tipo son (REQ
por requerido, OPT
por opcional,
una String como en :method => "GET"
indica valor por defecto y BOOL
para atributos
cuyo valor es su propio nombre.
En el código anterior se crean métodos form
, input
, texarea
, button
y br
en la clase HTMLForm
.
class XMLGrammar # Create an instance of this class, specifying a stream or object to # hold the output. This can be any object that responds to <<(String). def initialize(out) @out = out # Remember where to send our output end # Invoke the block in an instance that outputs to the specified stream. def self.generate(out, &block) new(out).instance_eval(&block) end # Define an allowed element (or tag) in the grammar. # This class method is the grammar-specification DSL # and defines the methods that constitute the XML-output DSL. def self.element(tagname, attributes={}) @allowed_attributes ||= {} @allowed_attributes[tagname] = attributes class_eval %Q{ def #{tagname}(attributes={}, &block) tag(:#{tagname},attributes,&block) end } end # These are constants used when defining attribute values. OPT = :opt # for optional attributes REQ = :req # for required attributes BOOL = :bool # for attributes whose value is their own name def self.allowed_attributes @allowed_attributes end # Output the specified object as CDATA, return nil. def content(text) @out << text.to_s nil end # Output the specified object as a comment, return nil. def comment(text) @out << "<!-- #{text} -->\n" nil end # Output a tag with the specified name and attribute. # If there is a block, invoke it to output or return content. # Return nil. def tag(tagname, attributes={}) # Output the tag name @out << "<#{tagname}" # Get the allowed attributes for this tag. allowed = self.class.allowed_attributes[tagname] # First, make sure that each of the attributes is allowed. # Assuming they are allowed, output all of the specified ones. attributes.each_pair do |key,value| raise "unknown attribute: #{key}" unless allowed.include?(key) @out << " #{key}='#{value}'" end # Now look through the allowed attributes, checking for # required attributes that were omitted and for attributes with # default values that we can output. allowed.each_pair do |key,value| # If this attribute was already output, do nothing. next if attributes.has_key? key if (value == REQ) raise "required attribute '#{key}' missing in <#{tagname}>" elsif value.is_a? String @out << " #{key}='#{value}'" end end if block_given? # This block has content @out << '>' # End the opening tag content = yield # Invoke the block to output or return content if content # If any content returned @out << content.to_s # Output it as a string end @out << "</#{tagname}>\n" # Close the tag else # Otherwise, this is an empty tag, so just close it. @out << "/>\n" end nil # Tags output themselves, so they don't return any content. end end
[~/chapter8ReflectionandMetaprogramming/DSL/xmlgenerator(master)]$ pwd -P /Users/casiano/Google Drive/src/ruby/TheRubyProgrammingLanguage/chapter8ReflectionandMetaprogramming/DSL/xmlgenerator
class XMLGrammar # Create an instance of this class, specifying a stream or object to # hold the output. This can be any object that responds to <<(String). def initialize(out) @out = out # Remember where to send our output end # Invoke the block in an instance that outputs to the specified stream. def self.generate(out, &block) new(out).instance_eval(&block) end # Define an allowed element (or tag) in the grammar. # This class method is the grammar-specification DSL # and defines the methods that constitute the XML-output DSL. def self.element(tagname, attributes={}) @allowed_attributes ||= {} @allowed_attributes[tagname] = attributes class_eval %Q{ def #{tagname}(attributes={}, &block) tag(:#{tagname},attributes,&block) end } end # These are constants used when defining attribute values. OPT = :opt # for optional attributes REQ = :req # for required attributes BOOL = :bool # for attributes whose value is their own name def self.allowed_attributes @allowed_attributes end # Output the specified object as CDATA, return nil. def content(text) @out << text.to_s nil end # Output the specified object as a comment, return nil. def comment(text) @out << "<!-- #{text} -->\n" nil end # Output a tag with the specified name and attribute. # If there is a block, invoke it to output or return content. # Return nil. def tag(tagname, attributes={}) # Output the tag name @out << "<#{tagname}" # Get the allowed attributes for this tag. allowed = self.class.allowed_attributes[tagname] # First, make sure that each of the attributes is allowed. # Assuming they are allowed, output all of the specified ones. attributes.each_pair do |key,value| raise "unknown attribute: #{key}" unless allowed.include?(key) @out << " #{key}='#{value}'" end # Now look through the allowed attributes, checking for # required attributes that were omitted and for attributes with # default values that we can output. allowed.each_pair do |key,value| # If this attribute was already output, do nothing. next if attributes.has_key? key if (value == REQ) raise "required attribute '#{key}' missing in <#{tagname}>" elsif value.is_a? String @out << " #{key}='#{value}'" end end if block_given? # This block has content @out << '>' # End the opening tag content = yield # Invoke the block to output or return content if content # If any content returned @out << content.to_s # Output it as a string end @out << "</#{tagname}>\n" # Close the tag else # Otherwise, this is an empty tag, so just close it. @out << "/>\n" end nil # Tags output themselves, so they don't return any content. end end class HTMLForm < XMLGrammar element :form, :action => REQ, :method => "GET", :enctype => "application/x-www-form-urlencoded", :name => OPT element :input, :type => "text", :name => OPT, :value => OPT, :maxlength => OPT, :size => OPT, :src => OPT, :checked => BOOL, :disabled => BOOL, :readonly => BOOL element :textarea, :rows => REQ, :cols => REQ, :name => OPT, :disabled => BOOL, :readonly => BOOL element :button, :name => OPT, :value => OPT, :type => "submit", :disabled => OPT element :br end HTMLForm.generate(STDOUT) do comment "This is a simple HTML form" form :name => "registration", :action => "http://www.example.com/register.cgi" do content "Name:" br input :name => "name" br content "Post:" br textarea :name => "address", :rows=>6, :cols=>40 do "Please enter your post here" end br button { "Submit" } end end
Casiano Rodriguez León 2015-01-07