/* Simple parser for CSS */

var CSSParser = Editor.Parser = (function() {
    var tokenizeCSS = (function() {
        function normal(source, setState) {
            var ch = source.next();
            if (ch == "@") {
                source.nextWhileMatches(/\w/);
                return "css-at";
            }
            else if (ch == "/" && source.equals("*")) {
                setState(inCComment);
                return null;
            }
            else if (ch == "<" && source.equals("!")) {
                setState(inSGMLComment);
                return null;
            }
            else if (ch == "=") {
                return "css-compare";
            }
            else if (source.equals("=") && (ch == "~" || ch == "|")) {
                source.next();
                return "css-compare";
            }
            else if (ch == "\"" || ch == "'") {
                setState(inString(ch));
                return null;
            }
            else if (ch == "#") {
                source.nextWhileMatches(/\w/);
                return "css-hash";
            }
            else if (ch == "!") {
                source.nextWhileMatches(/[ \t]/);
                source.nextWhileMatches(/\w/);
                return "css-important";
            }
            else if (/\d/.test(ch)) {
                source.nextWhileMatches(/[\w.%]/);
                return "css-unit";
            }
            else if (/[,.+>*\/]/.test(ch)) {
                return "css-select-op";
            }
            else if (/[;{}:\[\]]/.test(ch)) {
                return "css-punctuation";
            }
            else {
                source.nextWhileMatches(/[\w\\\-_]/);
                return "css-identifier";
            }
        }

        function inCComment(source, setState) {
            var maybeEnd = false;
            while (!source.endOfLine()) {
                var ch = source.next();
                if (maybeEnd && ch == "/") {
                    setState(normal);
                    break;
                }
                maybeEnd = (ch == "*");
            }
            return "css-comment";
        }

        function inSGMLComment(source, setState) {
            var dashes = 0;
            while (!source.endOfLine()) {
                var ch = source.next();
                if (dashes >= 2 && ch == ">") {
                    setState(normal);
                    break;
                }
                dashes = (ch == "-") ? dashes + 1 : 0;
            }
            return "css-comment";
        }

        function inString(quote) {
            return function(source, setState) {
                var escaped = false;
                while (!source.endOfLine()) {
                    var ch = source.next();
                    if (ch == quote && !escaped)
                        break;
                    escaped = !escaped && ch == "\\";
                }
                if (!escaped)
                    setState(normal);
                return "css-string";
            };
        }

        return function(source, startState) {
            return tokenizer(source, startState || normal);
        };
    })();

    function indentCSS(inBraces, inRule, base) {
        return function(nextChars) {
            if (!inBraces || /^\}/.test(nextChars)) return base;
            else if (inRule) return base + indentUnit * 2;
            else return base + indentUnit;
        };
    }

    // This is a very simplistic parser -- since CSS does not really
    // nest, it works acceptably well, but some nicer colouroing could
    // be provided with a more complicated parser.
    function parseCSS(source, basecolumn) {
        basecolumn = basecolumn || 0;
        var tokens = tokenizeCSS(source);
        var inBraces = false, inRule = false, inDecl = false;;

        var iter = {
            next: function() {
                var token = tokens.next(), style = token.style, content = token.content;

                if (style == "css-hash")
                    style = token.style =    inRule ? "css-colorcode" : "css-identifier";
                if (style == "css-identifier") {
                    if (inRule) token.style = "css-value";
                    else if (!inBraces && !inDecl) token.style = "css-selector";
                }

                if (content == "\n")
                    token.indentation = indentCSS(inBraces, inRule, basecolumn);

                if (content == "{" && inDecl == "@media")
                    inDecl = false;
                else if (content == "{")
                    inBraces = true;
                else if (content == "}")
                    inBraces = inRule = inDecl = false;
                else if (content == ";")
                    inRule = inDecl = false;
                else if (inBraces && style != "css-comment" && style != "whitespace")
                    inRule = true;
                else if (!inBraces && style == "css-at")
                    inDecl = content;

                return token;
            },

            copy: function() {
                var _inBraces = inBraces, _inRule = inRule, _tokenState = tokens.state;
                return function(source) {
                    tokens = tokenizeCSS(source, _tokenState);
                    inBraces = _inBraces;
                    inRule = _inRule;
                    return iter;
                };
            }
        };
        return iter;
    }

    return {make: parseCSS, electricChars: "}"};
})();
