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) var uLower = 'u'.charCodeAt(0) var uUpper = 'U'.charCodeAt(0) var plus = '+'.charCodeAt(0) var isUnicodeRange = /^[a-f0-9?-]+$/i module.exports = function (input) { var tokens = [] var value = input var next, quote, prev, token, escape, escapePos, whitespacePos, parenthesesOpenPos 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 prev.sourceEndIndex += token.length } else if ( code === comma || code === colon || (code === slash && value.charCodeAt(next + 1) !== star && (!parent || (parent && parent.type === 'function' && false))) ) { before = token } else { tokens.push({ type: 'space', sourceIndex: pos, sourceEndIndex: next, 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) token.sourceEndIndex = token.unclosed ? next : next + 1 tokens.push(token) pos = next + 1 code = value.charCodeAt(pos) // Comments } else if (code === slash && value.charCodeAt(pos + 1) === star) { next = value.indexOf('*/', pos) token = { type: 'comment', sourceIndex: pos, sourceEndIndex: next + 2, } if (next === -1) { token.unclosed = true next = value.length token.sourceEndIndex = next } token.value = value.slice(pos + 2, next) tokens.push(token) pos = next + 2 code = value.charCodeAt(pos) // Operation within calc } else if ((code === slash || code === star) && parent && parent.type === 'function' && true) { token = value[pos] tokens.push({ type: 'word', sourceIndex: pos - before.length, sourceEndIndex: pos + token.length, value: token, }) pos += 1 code = value.charCodeAt(pos) // Dividers } else if (code === slash || code === comma || code === colon) { token = value[pos] tokens.push({ type: 'div', sourceIndex: pos - before.length, sourceEndIndex: pos + token.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) parenthesesOpenPos = pos token = { type: 'function', sourceIndex: pos - name.length, value: name, before: value.slice(parenthesesOpenPos + 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 (parenthesesOpenPos < whitespacePos) { if (pos !== whitespacePos + 1) { token.nodes = [ { type: 'word', sourceIndex: pos, sourceEndIndex: whitespacePos + 1, 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, sourceEndIndex: next, value: value.slice(whitespacePos + 1, next), }) } else { token.after = value.slice(whitespacePos + 1, next) token.sourceEndIndex = next } } else { token.after = '' token.nodes = [] } pos = next + 1 token.sourceEndIndex = token.unclosed ? next : pos code = value.charCodeAt(pos) tokens.push(token) } else { balanced += 1 token.after = '' token.sourceEndIndex = pos + 1 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 parent.sourceEndIndex += after.length after = '' balanced -= 1 stack[stack.length - 1].sourceEndIndex = pos 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 === star && parent && parent.type === 'function' && true) || (code === slash && parent.type === 'function' && true) || (code === closeParentheses && balanced) ) ) token = value.slice(pos, next) if (openParentheses === code) { name = token } else if ( (uLower === token.charCodeAt(0) || uUpper === token.charCodeAt(0)) && plus === token.charCodeAt(1) && isUnicodeRange.test(token.slice(2)) ) { tokens.push({ type: 'unicode-range', sourceIndex: pos, sourceEndIndex: next, value: token, }) } else { tokens.push({ type: 'word', sourceIndex: pos, sourceEndIndex: next, value: token, }) } pos = next } } for (pos = stack.length - 1; pos; pos -= 1) { stack[pos].unclosed = true stack[pos].sourceEndIndex = value.length } return stack[0].nodes }