| 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; |
| }; |