var openParentheses = '('.charCodeAt(0); var closeParentheses = ')'.charCodeAt(0); var singleQuote = '\''.charCodeAt(0); var doubleQuote = '"'.charCodeAt(0); var backslash = '\\'.charCodeAt(0); var slash = '/'.charCodeAt(0); var comma = ','.charCodeAt(0); var colon = ':'.charCodeAt(0); var star = '*'.charCodeAt(0); module.exports = function (input) { var tokens = []; var value = input; var next, quote, prev, token, escape, escapePos, whitespacePos; var pos = 0; var code = value.charCodeAt(pos); var max = value.length; var stack = [{ nodes: tokens }]; var balanced = 0; var parent; var name = ''; var before = ''; var after = ''; while (pos < max) { // Whitespaces if (code <= 32) { next = pos; do { next += 1; code = value.charCodeAt(next); } while (code <= 32); token = value.slice(pos, next); prev = tokens[tokens.length - 1]; if (code === closeParentheses && balanced) { after = token; } else if (prev && prev.type === 'div') { prev.after = token; } else if (code === comma || code === colon || code === slash && value.charCodeAt(next + 1) !== star) { before = token; } else { tokens.push({ type: 'space', sourceIndex: pos, value: token }); } pos = next; // Quotes } else if (code === singleQuote || code === doubleQuote) { next = pos; quote = code === singleQuote ? '\'' : '"'; token = { type: 'string', sourceIndex: pos, quote: quote }; do { escape = false; next = value.indexOf(quote, next + 1); if (~next) { escapePos = next; while (value.charCodeAt(escapePos - 1) === backslash) { escapePos -= 1; escape = !escape; } } else { value += quote; next = value.length - 1; token.unclosed = true; } } while (escape); token.value = value.slice(pos + 1, next); tokens.push(token); pos = next + 1; code = value.charCodeAt(pos); // Comments } else if (code === slash && value.charCodeAt(pos + 1) === star) { token = { type: 'comment', sourceIndex: pos }; next = value.indexOf('*/', pos); if (next === -1) { token.unclosed = true; next = value.length; } token.value = value.slice(pos + 2, next); tokens.push(token); pos = next + 2; code = value.charCodeAt(pos); // Dividers } else if (code === slash || code === comma || code === colon) { token = value[pos]; tokens.push({ type: 'div', sourceIndex: pos - before.length, value: token, before: before, after: '' }); before = ''; pos += 1; code = value.charCodeAt(pos); // Open parentheses } else if (openParentheses === code) { // Whitespaces after open parentheses next = pos; do { next += 1; code = value.charCodeAt(next); } while (code <= 32); token = { type: 'function', sourceIndex: pos - name.length, value: name, before: value.slice(pos + 1, next) }; pos = next; if (name === 'url' && code !== singleQuote && code !== doubleQuote) { next -= 1; do { escape = false; next = value.indexOf(')', next + 1); if (~next) { escapePos = next; while (value.charCodeAt(escapePos - 1) === backslash) { escapePos -= 1; escape = !escape; } } else { value += ')'; next = value.length - 1; token.unclosed = true; } } while (escape); // Whitespaces before closed whitespacePos = next; do { whitespacePos -= 1; code = value.charCodeAt(whitespacePos); } while (code <= 32); if (pos !== whitespacePos + 1) { token.nodes = [{ type: 'word', sourceIndex: pos, value: value.slice(pos, whitespacePos + 1) }]; } else { token.nodes = []; } if (token.unclosed && whitespacePos + 1 !== next) { token.after = ''; token.nodes.push({ type: 'space', sourceIndex: whitespacePos + 1, value: value.slice(whitespacePos + 1, next) }); } else { token.after = value.slice(whitespacePos + 1, next); } pos = next + 1; code = value.charCodeAt(pos); tokens.push(token); } else { balanced += 1; token.after = ''; tokens.push(token); stack.push(token); tokens = token.nodes = []; parent = token; } name = ''; // Close parentheses } else if (closeParentheses === code && balanced) { pos += 1; code = value.charCodeAt(pos); parent.after = after; after = ''; balanced -= 1; stack.pop(); parent = stack[balanced]; tokens = parent.nodes; // Words } else { next = pos; do { if (code === backslash) { next += 1; } next += 1; code = value.charCodeAt(next); } while (next < max && !( code <= 32 || code === singleQuote || code === doubleQuote || code === comma || code === colon || code === slash || code === openParentheses || code === closeParentheses && balanced )); token = value.slice(pos, next); if (openParentheses === code) { name = token; } else { tokens.push({ type: 'word', sourceIndex: pos, value: token }); } pos = next; } } for (pos = stack.length - 1; pos; pos -= 1) { stack[pos].unclosed = true; } return stack[0].nodes; };