jade
Version:
Jade template engine
300 lines (262 loc) • 6.73 kB
JavaScript
// Sass - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
/**
* Library version.
*/
exports.version = '0.4.3'
/**
* Compiled sass cache.
*/
var cache = {}
/**
* Sass grammar tokens.
*/
var tokens = [
['indent', /^\n +/],
['space', /^ +/],
['nl', /^\n/],
['js', /^{(.*?)}/],
['comment', /^\/\/(.*)/],
['string', /^(?:'(.*?)'|"(.*?)")/],
['variable', /^!([\w\-]+) *= *([^\n]+)/],
['variable.alternate', /^([\w\-]+): +([^\n]+)/],
['property.expand', /^=([\w\-]+) *([^\n]+)/],
['property', /^:([\w\-]+) *([^\n]+)/],
['continuation', /^&(.+)/],
['mixin', /^\+([\w\-]+)/],
['selector', /^(.+)/]
]
/**
* Vendor-specific expansion prefixes.
*/
exports.expansions = ['-moz-', '-webkit-']
/**
* Tokenize the given _str_.
*
* @param {string} str
* @return {array}
* @api private
*/
function tokenize(str) {
var token, captures, stack = []
while (str.length) {
for (var i = 0, len = tokens.length; i < len; ++i)
if (captures = tokens[i][1].exec(str)) {
token = [tokens[i][0], captures],
str = str.replace(tokens[i][1], '')
break
}
if (token)
stack.push(token),
token = null
else
throw new Error("SyntaxError: near `" + str.slice(0, 25).replace('\n', '\\n') + "'")
}
return stack
}
/**
* Parse the given _tokens_, returning
* and hash containing the properties below:
*
* selectors: array of top-level selectors
* variables: hash of variables defined
*
* @param {array} tokens
* @return {hash}
* @api private
*/
function parse(tokens) {
var token, selector,
data = { variables: {}, mixins: {}, selectors: [] },
line = 1,
lastIndents = 0,
indents = 0
/**
* Output error _msg_ in context to the current line.
*/
function error(msg) {
throw new Error('ParseError: on line ' + line + '; ' + msg)
}
/**
* Reset parents until the indentation levels match.
*/
function reset() {
if (indents === 0)
return selector = null
while (lastIndents-- > indents)
selector = selector.parent
}
// Parse tokens
while (token = tokens.shift())
switch (token[0]) {
case 'mixin':
if (indents) {
var mixin = data.mixins[token[1][1]]
if (!mixin) error("mixin `" + token[1][1] + "' does not exist")
mixin.parent = selector
selector.children.push(mixin)
}
else
data.mixins[token[1][1]] = selector = new Selector(token[1][1], null, 'mixin')
break
case 'continuation':
reset()
selector = new Selector(token[1][1], selector, 'continuation')
break
case 'selector':
reset()
selector = new Selector(token[1][1], selector)
if (!selector.parent)
data.selectors.push(selector)
break
case 'property':
reset()
if (!selector) error('properties must be nested within a selector')
var val = token[1][2]
.replace(/!([\w\-]+)/g, function(orig, name){
return data.variables[name] || orig
})
.replace(/\{(.*?)\}/g, function(_, js){
with (data.variables){ return eval(js) }
})
selector.properties.push(new Property(token[1][1], val))
break
case 'property.expand':
exports.expansions.forEach(function(prefix){
tokens.unshift(['property', [, prefix + token[1][1], token[1][2]]])
})
break
case 'variable':
case 'variable.alternate':
data.variables[token[1][1]] = token[1][2]
break
case 'js':
with (data.variables){ eval(token[1][1]) }
break
case 'nl':
++line, indents = 0
break
case 'comment':
break
case 'indent':
++line
lastIndents = indents,
indents = (token[1][0].length - 1) / 2
if (indents > lastIndents &&
indents - 1 > lastIndents)
error('invalid indentation, to much nesting')
}
return data
}
/**
* Compile _selectors_ to a string of css.
*
* @param {array} selectors
* @return {string}
* @api private
*/
function compile(selectors) {
return selectors.join('\n')
}
/**
* Collect data by parsing _sass_.
* Returns a hash containing the following properties:
*
* selectors: array of top-level selectors
* variables: hash of variables defined
*
* @param {string} sass
* @return {hash}
* @api public
*/
exports.collect = function(sass) {
return parse(tokenize(sass))
}
/**
* Render a string of _sass_.
*
* Options:
*
* - filename Optional filename to aid in error reporting
* - cache Optional caching of compiled content. Requires "filename" option
*
* @param {string} sass
* @param {object} options
* @return {string}
* @api public
*/
exports.render = function(sass, options) {
options = options || {}
if (options.cache && !options.filename)
throw new Error('filename option must be passed when cache is enabled')
if (options.cache)
return cache[options.filename]
? cache[options.filename]
: cache[options.filename] = compile(exports.collect(sass).selectors)
return compile(exports.collect(sass).selectors)
}
// --- Selector
/**
* Initialize a selector with _string_ and
* optional _parent_.
*
* @param {string} string
* @param {Selector} parent
* @param {string} type
* @api private
*/
function Selector(string, parent, type) {
this.string = string
this.parent = parent
this.properties = []
this.children = []
if (type) this[type] = true
if (parent) parent.children.push(this)
}
/**
* Return selector string.
*
* @return {string}
* @api private
*/
Selector.prototype.selector = function() {
var selector = this.string
if (this.parent)
selector = this.continuation
? this.parent.selector() + selector
: this.mixin
? this.parent.selector()
: this.parent.selector() + ' ' + selector
return selector
}
/**
* Return selector and nested selectors as CSS.
*
* @return {string}
* @api private
*/
Selector.prototype.toString = function() {
return (this.properties.length
? this.selector() + ' {\n' + this.properties.join('\n') + '}\n'
: '') + this.children.join('')
}
// --- Property
/**
* Initialize property with _name_ and _val_.
*
* @param {string} name
* @param {string} val
* @api private
*/
function Property(name, val) {
this.name = name
this.val = val
}
/**
* Return CSS string representing a property.
*
* @return {string}
* @api private
*/
Property.prototype.toString = function() {
return ' ' + this.name + ': ' + this.val + ';'
}