Skip to content

Instantly share code, notes, and snippets.

@tj
Created December 18, 2009 19:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tj/259682 to your computer and use it in GitHub Desktop.
Save tj/259682 to your computer and use it in GitHub Desktop.
/* ex:ts=2:et: */
/*jslint bitwise: true, browser: true, eqeqeq: true, evil: true, immed: true, newcap: true,
nomen: true, plusplus: true, regexp: true, undef: true, white: true, indent: 2 */
/*globals */
var Haml = {};
// Bind to the exports object if it exists. (CommonJS and NodeJS)
if (exports) {
Haml = exports;
}
Haml.to_html = function (json) {
if (typeof json === 'string') {
if (json.substr(0, 3) === '!!!') {
switch (json.substr(4).toLowerCase()) {
case 'xml':
return "<?xml version='1.0' encoding='utf-8' ?>\n";
case '':
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n';
case '1.1':
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n';
case 'strict':
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n';
}
}
return json;
}
if (typeof json === 'object' && json.length !== undefined) {
if (typeof json[0] === 'object')
{
if (json[0].tag !== undefined) {
return (function () {
var tag, element, html, attributes;
element = json.shift();
tag = element.tag;
html = tag;
for (var key in element) {
if (element.hasOwnProperty(key) && key !== 'tag') {
html += " " + key + "=\"" + element[key].
replace("&", "&amp;").
replace("<", "&lt;").
replace(">", "&gt;").
replace("\"", "&quot;") + "\"";
}
}
if (json.length === 0 && (tag === "link" || tag === 'br' || tag === 'input')) {
return "\n<" + html + " />\n";
}
return "\n<" + html + ">" + Haml.to_html(json) + "</" + tag + ">\n";
}());
}
if (json[0].plugin !== undefined) {
switch (json.shift().plugin) {
case "javascript":
return '\n<script type="text/javascript">\n//<![CDATA[\n ' + json[0].split("\n").join("\n ") + "\n//]]>\n</script>\n";
case "css":
return '\n<style type="text/css">\n ' + json[0].split("\n").join("\n ") + "\n</style>\n";
}
}
}
return json.map(Haml.to_html).join('');
}
return JSON.stringify(json);
}
Haml.parse = function (text, locals) {
var empty_regex = new RegExp("^[ \t]*$"),
indent_regex = new RegExp("^ *"),
element_regex = new RegExp("^(?::[a-z]+|(?:[%][a-z][a-z0-9]*)?(?:[#.][a-z0-9_-]+)*)", "i"),
scope = this,
haml, element, stack, indent, buffer, old_indent, mode, last_insert;
// Sortof an instance_eval for Javascript
function instance_eval(input) {
var block;
block = function () { with(scope, locals || {}) { return eval("(" + input + ")"); } };
return block.call(scope);
}
function process_embedded_code(string) {
if (typeof string !== 'string') {
return string;
}
var matches = string.match(/#{([^}]*)}/g);
if (matches) {
matches.forEach(function (match) {
var pair = match.match(/#{([^}]*)}/);
string = string.replace(pair[0], instance_eval(pair[1]));
})
}
return string;
}
function flush_buffer() {
if (buffer.length > 0) {
mode = "NORMAL";
element.push(process_embedded_code(buffer.join("\n")));
buffer = [];
}
}
function parse_push() {
stack.push({element: element, mode: mode});
var new_element = [];
mode = "NORMAL";
element.push(new_element);
element = new_element;
}
function parse_pop() {
var last = stack.pop();
if (element.length === 1) {
if (typeof element[0] === "string") {
if (element[0].match(element_regex)[0].length === 0) {
// Collapse arrays with single string literal
last.element[last.element.length - 1] = element[0];
}
}
}
element = last.element;
mode = last.mode;
}
function get_indent(line) {
if (line === undefined) {
return 0;
}
var i = line.match(indent_regex);
return i[0].length / 2;
}
function parse_attribs(line) {
// Parse the attribute block using a state machine
if (!(line.length > 0 && line.charAt(0) === '{')) {
return line;
}
var l = line.length;
var count = 1;
var quote = false;
var skip = false;
for (var i = 1; count > 0; i += 1) {
// If we reach the end of the line, then there is a problem
if (i > l) {
throw "Malformed attribute block";
}
var c = line.charAt(i);
if (skip) {
skip = false;
} else {
if (quote) {
if (c === '\\') {
skip = true;
}
if (c === quote) {
quote = false;
}
} else {
if (c === '"' || c === "'") {
quote = c;
}
if (c === '{') {
count += 1;
}
if (c === '}') {
count -= 1;
}
}
}
}
var block = line.substr(0, i);
(function () {
element.push(instance_eval(block))
}.call(this));
return line.substr(i);
}
function parse_content(line) {
// Strip off leading whitespace
line = line.replace(indent_regex, '');
// Ignore blank lines
if (line.length === 0) {
return;
}
if (mode === 'ELEMENT') {
parse_pop();
}
switch (line.charAt(0)) {
case '/':
break;
case '=':
(function () {
buffer.push(instance_eval(line.substr(1)));
}.call(this));
break;
case "\\":
buffer.push(line.substr(1));
break;
default:
buffer.push(line);
break;
}
}
function parse_element(line, selector) {
flush_buffer();
if (element.length > 0) {
if (mode === 'ELEMENT') {
parse_pop();
}
parse_push();
}
mode = 'ELEMENT';
var classes = selector.match(/\.[^\.#]+/g),
ids = selector.match(/#[^\.#]+/g),
tag = selector.match(/^%([^\.#]+)/g),
plugin = selector.match(/^:([^\.#]+)/g);
tag = tag ? tag[0].substr(1) : (plugin ? null : 'div');
plugin = plugin ? plugin[0].substr(1) : null;
line = parse_attribs.call(this, line.substr(selector.length));
var attrs;
if (typeof element[element.length - 1] === "object") {
attrs = element[element.length - 1];
} else {
attrs = {};
element.push(attrs);
}
if (tag) {
attrs.tag = tag;
}
if (plugin) {
attrs.plugin = plugin;
}
if (ids) {
for (var i = 0, l = ids.length; i < l; i += 1) {
ids[i] = ids[i].substr(1);
}
if (attrs.id) {
ids.push(attrs.id);
}
attrs.id = ids.join(" ");
}
if (classes) {
for (var i = 0, l = classes.length; i < l; i += 1) {
classes[i] = classes[i].substr(1);
}
if (attrs['class']) {
classes.push(attrs['class']);
}
attrs['class'] = classes.join(" ");
}
if (selector.charAt(0) === ':') {
mode = 'RAW';
} else {
if (!line.match(empty_regex)) {
parse_push();
parse_content.call(this, line, true);
flush_buffer();
parse_pop();
}
}
}
function process_plugins() {
var contents, i;
switch (element[0].plugin) {
case 'if':
var condition = element[0].condition;
contents = element[1];
for (i in element) {
if (element.hasOwnProperty(i)) {
delete element[i];
}
}
if (condition) {
var new_element = Haml.parse.call(this, contents);
for (i in new_element) {
if (new_element.hasOwnProperty(i)) {
element[i] = new_element[i];
}
}
element.length = new_element.length;
}
break;
case 'foreach':
var array, key, value, key_name, value_name;
array = element[0].array;
key_name = element[0].key;
value_name = element[0].value;
contents = element[1];
for (i in element) {
if (element.hasOwnProperty(i)) {
delete element[i];
}
}
element.length = 0;
for (key in array) {
if (array.hasOwnProperty(key)) {
value = array[key];
this[key_name] = key;
this[value_name] = value;
element.push(Haml.parse.call(this, contents));
}
}
break;
}
}
haml = [];
element = haml;
stack = [];
buffer = [];
mode = 'NORMAL';
parse_push(); // Prime the pump so we can have multiple root elements
indent = 0;
old_indent = indent;
var lines = text.split("\n"),
line, line_index, line_count;
line_count = lines.length;
for (line_index = 0; line_index <= line_count; line_index += 1) {
line = lines[line_index];
switch (mode) {
case 'ELEMENT':
case 'NORMAL':
// Do indentation
indent = get_indent(line);
if (indent === old_indent + 1) {
parse_push();
old_indent = indent;
}
while (indent < old_indent) {
flush_buffer();
parse_pop();
old_indent -= 1;
}
if (line === undefined) {
continue;
}
line = line.substr(indent * 2);
// Pass doctype commands through as is
if (line.substr(0, 3) === '!!!') {
element.push(line);
continue;
}
// Check for special element characters
var match = line.match(element_regex);
if (match && match[0].length > 0) {
parse_element.call(this, line, match[0]);
} else {
parse_content.call(this, line);
}
break;
case 'RAW':
if (get_indent(line) <= indent) {
flush_buffer();
process_plugins.call(this);
mode = "ELEMENT";
line_index -= 1;
continue;
}
line = line.substr((indent + 1) * 2);
buffer.push(line);
break;
}
}
if (haml.length === 1 && typeof haml[0] !== 'string') {
haml = haml[0];
}
return haml;
};
Haml.render = function(text, options) {
options = options || {};
var json = Haml.parse.call(options.context || GLOBAL, text, options.locals);
return Haml.to_html(json).replace('\n\n', '\n');
}
// Sass - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
/**
* Sass grammar tokens.
*/
var tokens = [
['indent', /^\n +/],
['space', /^ +/],
['nl', /^\n/],
[',', /^,/],
['&', /^&/],
['string', /^'(.*?)'|"(.*?)"/],
['variable', /^!([\w\-]+) *= *([^\n]+)/],
['property', /^:([\w\-]+) *([^\n]+)/],
['selector', /^([\w\-\.:]+)/]
]
/**
* 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 "SyntaxError: near `" + str.slice(0, 25).replace('\n', '\\n') + "'"
}
return stack
}
/**
* Parse the given _tokens_, returning
* and array of top-level selectors.
*
* @param {array} tokens
* @return {array}
* @api private
*/
function parse(tokens) {
var token, selector,
variables = {},
line = 1,
lastIndents = 0,
indents = 0,
selectors = []
function error(msg) {
throw 'ParseError: near line ' + line + '; ' + msg
}
while (token = tokens.shift())
switch (token[0]) {
case 'selector':
selector = new Selector(token[1][1], selector)
if (!selector.parent)
selectors.push(selector)
// TODO: reset parents. come up with solution
break
case 'property':
var val = token[1][2].replace(/!([\w\-]+)/, function(orig, name){
return variables[name] || orig
})
selector.properties.push(new Property(token[1][1], val))
break
case 'variable':
variables[token[1][1]] = token[1][2]
break
case 'nl':
++line, indents = 0
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 selectors
}
function compile(selectors) {
return selectors.join('\n')
}
// --- Selector
/**
* Initialize a selector with _string_ and
* optional _parent_.
*
* @param {string} string
* @param {Selector} parent
* @api private
*/
function Selector(string, parent) {
this.string = string
this.parent = parent
this.properties = []
this.children = []
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.parent.selector() + ' ' + selector
return selector
}
/**
* Return selector and nested selectors as CSS.
*
* @return {string}
* @api private
*/
Selector.prototype.toString = function() {
var str = this.selector() + ' {\n'
for (var i = 0, len = this.properties.length; i < len; ++i)
str += this.properties[i] + '\n'
str += '}\n'
for (var i = 0, len = this.children.length; i < len; ++i)
str += this.children[i]
return str
}
// --- 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 + ';'
}
print(compile(parse(tokenize(readFile('examples/style.sass')))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment