UNPKG

haml-coffee

Version:

Haml templates where you can write inline CoffeeScript.

395 lines (337 loc) 14.1 kB
Node = require('./node') {escapeQuotes} = require('../util/text') # HAML node that contains Haml a haml tag that can have attributes # and a text or code assignment. There are shortcuts for id and class # generation and some special logic for merging attributes into existing # ids and classes. # # @example Haml tag # %footer => <footer></footer> # # @example Haml id # #content => <div id='content'></div> # %span#status{ :id => @user.status } => <span id='status_#{ @user.status }'></span> # # @example Haml classes # .hidden => <div class='hidden'></div> # %span.large.hidden => <span class='large hidden'></span> # .large{ :class => @user.role } => <div class='large #{ @user.role }'></div> # # Haml HTML attributes are very limited and allows only simple string # (with interpolation) or variable assignment to an attribute. # # @example Haml HTML attributes # %p(class='hidden') => <p class='hidden'><p> # #account(class=@status) => <div id='account' class='#{ status }'></div> # .logout(title="Logout #{ user.name }") => <div class='logout' title='Logout #{ user.name }'></div> # # Ruby HTML attributes are more powerful and allows in addition to the # HTML attributes function calls: # # @example Haml Ruby attributes # %p{ :class => App.user.get('role') } => <p class='#{ App.user.get('role') }'></p> # module.exports = class Haml extends Node # Evaluate the node content and store the opener tag # and the closer tag if applicable. # evaluate: -> tokens = @parseExpression(@expression) # Evaluate Haml doctype if tokens.doctype @opener = @markText "#{ escapeQuotes(@buildDocType(tokens.doctype)) }" # Evaluate Haml tag else # Create a Haml node that can contain child nodes if @isNotSelfClosing(tokens.tag) prefix = escapeQuotes(@buildHtmlTagPrefix(tokens)) # Add Haml tag that contains a code assignment will be closed immediately if tokens.assignment match = tokens.assignment.match /^(=|!=|&=|~)\s*(.*)$/ identifier = match[1] assignment = match[2] if identifier is '~' code = "\#{$fp #{ assignment } }" # Code block with escaped code block, either `=` in escaped mode or `&=` else if identifier is '&=' or (identifier is '=' and @escapeHtml) if @preserve if @cleanValue code = "\#{ $p($e($c(#{ assignment }))) }" else code = "\#{ $p($e(#{ assignment })) }" else if @cleanValue code = "\#{ $e($c(#{ assignment })) }" else code = "\#{ $e(#{ assignment }) }" # Code block with unescaped output, either with `!=` or escaped mode to false else if identifier is '!=' or (identifier is '=' and not @escapeHtml) if @preserve if @cleanValue code = "\#{ $p($c(#{ assignment })) }" else code = "\#{ $p(#{ assignment }) }" else if @cleanValue code = "\#{ $c(#{ assignment }) }" else code = "\#{ #{ assignment } }" @opener = @markText "#{ prefix }>#{ code }" @closer = @markText "</#{ tokens.tag }>" # A Haml tag that contains an inline text else if tokens.text @opener = @markText "#{ prefix }>#{ tokens.text }" @closer = @markText "</#{ tokens.tag }>" # A Haml tag that can get more child nodes else @opener = @markText prefix + '>' @closer = @markText "</#{ tokens.tag}>" # Create a self closing tag that depends on the format `<br>` or `<br/>` else tokens.tag = tokens.tag.replace /\/$/, '' prefix = escapeQuotes(@buildHtmlTagPrefix(tokens)) @opener = @markText "#{ prefix }#{ if @format is 'xhtml' then ' /' else '' }>" # Parses the expression and detect the tag, attributes # and any assignment. In addition class and id cleanup # is performed according the the Haml spec: # # * Classes are merged together # * When multiple ids are provided, the last one is taken, # except they are defined in shortcut notation and attribute # notation. In this case, they will be combined, separated by # underscore. # # @example Id merging # #user{ :id => @user.id } => <div id='user_#{ @user.id }'></div> # # @param [String] exp the HAML expression # @return [Object] the parsed tag and options tokens # parseExpression: (exp) -> tag = @parseTag(exp) @preserve = true if @preserveTags.indexOf(tag.tag) isnt -1 id = tag.ids?.pop() classes = tag.classes attributes = [] # Clean attributes if tag.attributes for attribute in tag.attributes if attribute.key is 'id' if id # Merge attribute id into existing id id += '_' + attribute.value else # Push id from attribute id = attribute.value # Merge classes else if attribute.key is 'class' classes or= [] classes.push attribute.value # Add to normal attributes else attributes.push attribute { doctype : tag.doctype tag : tag.tag id : id classes : classes text : tag.text attributes : attributes assignment : tag.assignment } # Parse a tag line. This recognizes DocType tags `!!!` and # HAML tags like `#id.class text`. # # It also parses the code assignment `=`, `}=` and `)=` or # inline text and the whitespace removal markers `<` and `>`. # # @param [String] exp the HAML expression # @return [Object] the parsed tag tokens # parseTag: (exp) -> try doctype = exp.match(/^(\!{3}.*)/)?[1] return { doctype: doctype } if doctype # Separate Haml tags and inline text tokens = exp.match /^((?:[#%\.][a-z0-9_:\-]*[\/]?)+)(?:([\(\{].*[\)\}])?([\<\>]{0,2})(?=[=&!~])(.*)?|([\(\{].*[\)\}])?([\<\>]{0,2}))(.*)?/i haml = tokens[1] attributes = tokens[2] || tokens[5] whitespace = tokens[3] || tokens[6] assignment = tokens[4] || tokens[7] # Process inline text or assignment if assignment and not assignment.match(/^(=|!=|&=|~)/) text = assignment.replace(/^ /, '') assignment = undefined # Set whitespace removal markers if whitespace @wsRemoval.around = true if whitespace.indexOf('>') isnt -1 if whitespace.indexOf('<') isnt -1 @wsRemoval.inside = true @preserve = true # Extracts tag name, id and classes tag = haml.match(/\%([a-z_\-][a-z0-9_:\-]*[\/]?)/i) ids = haml.match(/\#([a-z_\-][a-z0-9_\-]*)/gi) classes = haml.match(/\.([a-z0-9_\-]*)/gi) { tag : if tag then tag[1] else 'div' ids : (id.substr(1) for id in ids) if ids classes : (klass.substr(1) for klass in classes) if classes attributes : @parseAttributes(attributes) assignment : assignment text : text } catch error throw "Unable to parse tag from #{ exp }: #{ error }" # Parse attributes either in Ruby style `%tag{ :attr => 'value' }` # or HTML style `%tag(attr='value)`. Both styles can be mixed: # `%tag(attr='value){ :attr => 'value' }`. # # This takes also care of proper attribute interpolation, unwrapping # quoted keys and value, e.g. `'a' => 'hello'` becomes `a => hello`. # # @param [String] exp the HAML expression # @return [Array] the parsed attribute tokens # parseAttributes: (exp) -> attributes = [] return attributes if exp is undefined [exp, datas] = @getDataAttributes(exp) findAttributes = /// (?: # HTML attributes ([-\w]+[\w:-]*\w?|'\w+[\w:-]*\w?'|"\w+[\w:-]*\w?")\s*=\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[\w@.]+) # Ruby 1.8 attributes | (:\w+[\w:-]*\w?|'[-\w]+[\w:-]*\w?'|"[-\w]+[\w:-]*\w?")\s*=>\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^},]+) # Ruby 1.9 attributes | (\w+[\w:-]*\w?|'[-\w]+[\w:-]*\w?'|"[-\w]+[\w:-]*\w?"):\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^},]+) ) ///g # Prepare all attributes while match = findAttributes.exec(exp) key = (match[1] || match[3] || match[5]).replace /^:/, '' value = match[2] || match[4] || match[6] bool = false # Ignore attributes some attribute values if ['false', ''].indexOf(value) is -1 # Set key to value if the value is boolean true if ['true'].indexOf(value) isnt -1 value = "'#{ key }'" bool = true # Wrap plain attributes into an interpolation, expect boolean values else if not value.match /^("|').*\1$/ if @escapeAttributes if @cleanValue value = '\'#{ $e($c(' + value + ')) }\'' else value = '\'#{ $e(' + value + ') }\'' else if @cleanValue value = '\'#{ $c(' + value + ') }\'' else value = '\'#{ (' + value + ') }\'' # Unwrap value from quotes value = quoted[2] if quoted = value.match /^("|')(.*)\1$/ # Unwrap key from quotes key = quoted[2] if quoted = key.match /^("|')(.*)\1$/ attributes.push { key : key value : value bool : bool } attributes.concat(datas) # Extracts the data attributes. # # @example data attribute # `:data => { :test => '123' }` # # @param [String] exp the expression to check # @return [Array<String, Array>] the expressions and data attributes # getDataAttributes: (exp) -> data = (/:?data:?\s*(?:=>\s*)?\{([^}]*)\},?/gi).exec(exp) return [exp, []] unless data?[1] exp = exp.replace(data[0], '') attributes = @parseAttributes(data[1]) for attribute in attributes attribute.key = "data-#{ attribute.key }" [exp, attributes] # Build the HTML tag prefix by concatenating all the # tag information together. The result is an unfinished # html tag that must be further processed: # # @example Prefix tag # <a id='id' class='class' attr='value' # # The Haml spec sorts the `class` names, even when they # contain interpolated classes. This is supported by # sorting classes at template render time. # # @example Template render time sorting # <p class='#{ [@user.name(), 'show'].sort().join(' ') }'> # # @param [Object] tokens all parsed tag tokens # @return [String] the tag prefix # buildHtmlTagPrefix: (tokens) -> tagParts = ["<#{ tokens.tag }"] # Set tag classes if tokens.classes classes = tokens.classes.sort().join ' ' # Class sort when the JST template is rendered if tokens.classes.length > 1 && classes.match /#\{/ classes = '#{ [' for klass in tokens.classes if interpolation = klass.match /#{(.*)}/ classes += "(#{ interpolation[1] })," else classes += "'#{ klass }'," classes += '].sort().join(\' \') }' tagParts.push "class='#{ classes }'" # Set tag id tagParts.push "id='#{ tokens.id }'" if tokens.id # Construct tag attributes if tokens.attributes for attribute in tokens.attributes if attribute.bool and @format is 'html5' tagParts.push "#{ attribute.key }" else tagParts.push "#{ attribute.key }=#{ @quoteAttributeValue(attribute.value) }" tagParts.join(' ') # Quote the attribute value, depending on its # content. # # @param [String] value the without start and end quote # @return [String] the quoted value # quoteAttributeValue: (value) -> if value.indexOf("'") is -1 quoted = "'#{ value }'" else quoted = "\"#{ value }\"" quoted # Build the DocType string depending on the `!!!` token # and the currently used HTML format. # # @param [String] doctype the HAML doctype # @return [String] the HTML doctype # buildDocType: (doctype) -> switch "#{ @format } #{ doctype }" when 'xhtml !!! XML' then '<?xml version=\'1.0\' encoding=\'utf-8\' ?>' when 'xhtml !!!' then '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' when 'xhtml !!! 1.1' then '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' when 'xhtml !!! mobile' then '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">' when 'xhtml !!! basic' then '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">' when 'xhtml !!! frameset' then '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">' when 'xhtml !!! 5', 'html5 !!!' then '<!DOCTYPE html>' when 'html5 !!! XML', 'html4 !!! XML' then '' when 'html4 !!!' then '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">' when 'html4 !!! frameset' then '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">' when 'html4 !!! strict' then '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' # Test if the given tag is a non-self enclosing tag, by # matching against a fixed tag list or parse for the self # closing slash `/` at the end. # # @param [String] tag the tag name without brackets # @return [Boolean] true when a non self closing tag # isNotSelfClosing: (tag) -> @selfCloseTags.indexOf(tag) == -1 && !tag.match(/\/$/)