haml-coffee
Version:
Haml templates where you can write inline CoffeeScript.
395 lines (337 loc) • 14.1 kB
text/coffeescript
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(/\/$/)