diff --git a/.gitignore b/.gitignore index f8a9d8f934..af37864181 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # Miscellaneous stuff +/sassc +/sass-spec + VERSION .DS_Store .sass-cache diff --git a/src/ast.cpp b/src/ast.cpp index 64a7b61368..b0f05ab9d8 100644 --- a/src/ast.cpp +++ b/src/ast.cpp @@ -7,8 +7,11 @@ #include "color_maps.hpp" #include #include -#include #include +#include +#include +#include +#include namespace Sass { @@ -25,6 +28,80 @@ namespace Sass { dynamic_cast(cond); } + std::string & str_ltrim(std::string & str) + { + auto it2 = std::find_if( str.begin() , str.end() , [](char ch){ return !std::isspace(ch , std::locale::classic() ) ; } ); + str.erase( str.begin() , it2); + return str; + } + + std::string & str_rtrim(std::string & str) + { + auto it1 = std::find_if( str.rbegin() , str.rend() , [](char ch){ return !std::isspace(ch , std::locale::classic() ) ; } ); + str.erase( it1.base() , str.end() ); + return str; + } + + void String_Constant::rtrim() + { + value_ = str_rtrim(value_); + } + void String_Constant::ltrim() + { + value_ = str_ltrim(value_); + } + void String_Constant::trim() + { + rtrim(); + ltrim(); + } + + void String_Schema::rtrim() + { + if (!empty()) { + if (String* str = dynamic_cast(last())) str->rtrim(); + } + } + void String_Schema::ltrim() + { + if (!empty()) { + if (String* str = dynamic_cast(first())) str->ltrim(); + } + } + void String_Schema::trim() + { + rtrim(); + ltrim(); + } + + bool At_Root_Query::exclude(std::string str) + { + bool with = feature() && unquote(feature()->to_string()).compare("with") == 0; + List* l = static_cast(value()); + std::string v; + + if (with) + { + if (!l || l->length() == 0) return str.compare("rule") != 0; + for (size_t i = 0, L = l->length(); i < L; ++i) + { + v = unquote((*l)[i]->to_string()); + if (v.compare("all") == 0 || v == str) return false; + } + return true; + } + else + { + if (!l || !l->length()) return str.compare("rule") == 0; + for (size_t i = 0, L = l->length(); i < L; ++i) + { + v = unquote((*l)[i]->to_string()); + if (v.compare("all") == 0 || v == str) return true; + } + return false; + } + } + void AST_Node::update_pstate(const ParserState& pstate) { pstate_.offset += pstate - pstate_ + pstate.offset; diff --git a/src/ast.hpp b/src/ast.hpp index 892af02cfc..269b31fe0b 100644 --- a/src/ast.hpp +++ b/src/ast.hpp @@ -1471,6 +1471,9 @@ namespace Sass { { concrete_type(STRING); } static std::string type_name() { return "string"; } virtual ~String() = 0; + virtual void rtrim() = 0; + virtual void ltrim() = 0; + virtual void trim() = 0; virtual bool operator==(const Expression& rhs) const = 0; ATTACH_OPERATIONS() }; @@ -1499,6 +1502,9 @@ namespace Sass { } return false; } + virtual void rtrim(); + virtual void ltrim(); + virtual void trim(); virtual size_t hash() { @@ -1539,6 +1545,9 @@ namespace Sass { std::string type() { return "string"; } static std::string type_name() { return "string"; } virtual bool is_invisible() const; + virtual void rtrim(); + virtual void ltrim(); + virtual void trim(); virtual size_t hash() { @@ -1698,40 +1707,13 @@ namespace Sass { ///////////////////////////////////////////////// class At_Root_Query : public Expression { private: - ADD_PROPERTY(String*, feature) + ADD_PROPERTY(Expression*, feature) ADD_PROPERTY(Expression*, value) - ADD_PROPERTY(bool, is_interpolated) public: - At_Root_Query(ParserState pstate, String* f = 0, Expression* v = 0, bool i = false) - : Expression(pstate), feature_(f), value_(v), is_interpolated_(i) + At_Root_Query(ParserState pstate, Expression* f = 0, Expression* v = 0, bool i = false) + : Expression(pstate), feature_(f), value_(v) { } - bool exclude(std::string str) - { - bool with = feature() && unquote(feature()->to_string()).compare("with") == 0; - List* l = static_cast(value()); - std::string v; - - if (with) - { - if (!l || l->length() == 0) return str.compare("rule") != 0; - for (size_t i = 0, L = l->length(); i < L; ++i) - { - v = unquote((*l)[i]->to_string()); - if (v.compare("all") == 0 || v == str) return false; - } - return true; - } - else - { - if (!l || !l->length()) return str.compare("rule") == 0; - for (size_t i = 0, L = l->length(); i < L; ++i) - { - v = unquote((*l)[i]->to_string()); - if (v.compare("all") == 0 || v == str) return true; - } - return false; - } - } + bool exclude(std::string str); ATTACH_OPERATIONS() }; diff --git a/src/constants.cpp b/src/constants.cpp index cad7a91376..31b009685a 100644 --- a/src/constants.cpp +++ b/src/constants.cpp @@ -89,6 +89,8 @@ namespace Sass { extern const char expression_kwd[] = "expression"; extern const char calc_fn_kwd[] = "calc"; + extern const char almost_any_value_class[] = "\"'#!;{}"; + // css selector keywords extern const char sel_deep_kwd[] = "/deep/"; diff --git a/src/constants.hpp b/src/constants.hpp index bd4bae3c63..568a28f76b 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -89,6 +89,9 @@ namespace Sass { extern const char expression_kwd[]; extern const char calc_fn_kwd[]; + // char classes for "regular expressions" + extern const char almost_any_value_class[]; + // css selector keywords extern const char sel_deep_kwd[]; diff --git a/src/eval.cpp b/src/eval.cpp index 923818ffdb..5e1f11c19c 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -1186,8 +1186,10 @@ namespace Sass { if (!dynamic_cast((*s)[0]) && !dynamic_cast((*s)[L - 1])) { if (String_Constant* l = dynamic_cast((*s)[0])) { if (String_Constant* r = dynamic_cast((*s)[L - 1])) { - if (l->value()[0] == '"' && r->value()[r->value().size() - 1] == '"') into_quotes = true; - if (l->value()[0] == '\'' && r->value()[r->value().size() - 1] == '\'') into_quotes = true; + if (r->value().size() > 0) { + if (l->value()[0] == '"' && r->value()[r->value().size() - 1] == '"') into_quotes = true; + if (l->value()[0] == '\'' && r->value()[r->value().size() - 1] == '\'') into_quotes = true; + } } } } diff --git a/src/parser.cpp b/src/parser.cpp index 1d99040ef1..07d5a3cf8c 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -251,6 +251,8 @@ namespace Sass { // generic at keyword (keep last) else if (lex< re_special_directive >(true)) { (*block) << parse_special_directive(); } + else if (lex< re_prefixed_directive >(true)) { (*block) << parse_prefixed_directive(); } + else if (lex< at_keyword >(true)) { (*block) << parse_directive(); } else if (block->is_root()) { lex< css_whitespace >(); @@ -2143,10 +2145,11 @@ namespace Sass { At_Root_Query* expr = 0; Lookahead lookahead_result; LOCAL_FLAG(in_at_root, true); - if (lex< exactly<'('> >()) { + if (lex_css< exactly<'('> >()) { expr = parse_at_root_query(); } - if (peek < exactly<'{'> >()) { + if (peek_css < exactly<'{'> >()) { + lex (); body = parse_block(true); } else if ((lookahead_result = lookahead_for_selector(position)).found) { @@ -2167,19 +2170,21 @@ namespace Sass { css_error("Invalid CSS", " after ", ": expected \"with\" or \"without\", was "); } - Declaration* declaration = parse_declaration(); - List* value = SASS_MEMORY_NEW(ctx.mem, List, declaration->value()->pstate(), 1); + Expression* feature = parse_list(); + if (!lex_css< exactly<':'> >()) error("style declaration must contain a value", pstate); + Expression* expression = parse_list(); + List* value = SASS_MEMORY_NEW(ctx.mem, List, feature->pstate(), 1); - if (declaration->value()->concrete_type() == Expression::LIST) { - value = static_cast(declaration->value()); + if (expression->concrete_type() == Expression::LIST) { + value = static_cast(expression); } - else *value << declaration->value(); + else *value << expression; At_Root_Query* cond = SASS_MEMORY_NEW(ctx.mem, At_Root_Query, - declaration->pstate(), - declaration->property(), - value); - if (!lex< exactly<')'> >()) error("unclosed parenthesis in @at-root expression", pstate); + value->pstate(), + feature, + value); + if (!lex_css< exactly<')'> >()) error("unclosed parenthesis in @at-root expression", pstate); return cond; } @@ -2212,6 +2217,166 @@ namespace Sass { return at_rule; } + Directive* Parser::parse_prefixed_directive() + { + std::string kwd(lexed); + + if (lexed == "@else") error("Invalid CSS: @else must come after @if", pstate); + + Directive* at_rule = SASS_MEMORY_NEW(ctx.mem, Directive, pstate, kwd); + Lookahead lookahead = lookahead_for_include(position); + if (lookahead.found && !lookahead.has_interpolants) { + at_rule->selector(parse_selector_list(true)); + } + + lex < css_comments >(false); + + if (lex < static_property >()) { + at_rule->value(parse_interpolated_chunk(Token(lexed))); + } else if (!(peek < alternatives < exactly<'{'>, exactly<'}'>, exactly<';'> > >())) { + at_rule->value(parse_list()); + } + + lex < css_comments >(false); + + if (peek< exactly<'{'> >()) { + at_rule->block(parse_block()); + } + + return at_rule; + } + + + Directive* Parser::parse_directive() + { + Directive* directive = SASS_MEMORY_NEW(ctx.mem, Directive, pstate, lexed); + Expression* val = parse_almost_any_value(); + // strip left and right if they are of type string + // debug_ast(val); + // std::cerr << "HAASDASD\n"; + directive->value(val); + if (peek< exactly<'{'> >()) { + directive->block(parse_block()); + } else if (!val) { + css_error("most def"); + } + return directive; + } + + Expression* Parser::lex_interpolation() + { + if (lex < interpolant >(true) != NULL) { + return parse_interpolated_chunk(lexed, true); + } + return 0; + } + + Expression* Parser::lex_interp_uri() + { + // create a string schema by lexing optional interpolations + return lex_interp< re_string_uri_open, re_string_uri_close >(); + } + + Expression* Parser::lex_interp_string() + { + Expression* rv = 0; + if ((rv = lex_interp< re_string_double_open, re_string_double_close >()) != NULL) return rv; + if ((rv = lex_interp< re_string_single_open, re_string_single_close >()) != NULL) return rv; + return rv; + } + + Expression* Parser::lex_almost_any_value_chars() + { + const char* match = + lex < + one_plus < + alternatives < + sequence < + exactly <'\\'>, + any_char + >, + sequence < + negate < + sequence < + exactly < url_kwd >, + exactly <'('> + > + >, + neg_class_char < + almost_any_value_class + > + >, + sequence < + exactly <'/'>, + negate < + alternatives < + exactly <'/'>, + exactly <'*'> + > + > + >, + sequence < + exactly <'\\'>, + exactly <'#'>, + negate < + exactly <'{'> + > + >, + sequence < + exactly <'!'>, + negate < + alpha + > + > + > + > + >(false); + if (match) { + // std::cerr << "[[" << std::string(lexed) << "]\n"; + return SASS_MEMORY_NEW(ctx.mem, String_Constant, pstate, lexed); + } + return NULL; + } + + Expression* Parser::lex_almost_any_value_token() + { + Expression* rv = 0; + if (*position == 0) return 0; + if ((rv = lex_almost_any_value_chars()) != NULL) return rv; + // if ((rv = lex_block_comment()) != NULL) return rv; + // if ((rv = lex_single_line_comment()) != NULL) return rv; + if ((rv = lex_interp_string()) != NULL) return rv; + if ((rv = lex_interp_uri()) != NULL) return rv; + if ((rv = lex_interpolation()) != NULL) return rv; + return rv; + } + + String_Schema* Parser::parse_almost_any_value() + { + + String_Schema* schema = SASS_MEMORY_NEW(ctx.mem, String_Schema, pstate); + if (*position == 0) return 0; + lex < spaces >(false); + Expression* token = lex_almost_any_value_token(); + if (!token) return 0; + // std::cerr << "LEX [" << std::string(lexed) << "]\n"; + *schema << token; + if (*position == 0) { + schema->rtrim(); + return schema; + } + + while ((token = lex_almost_any_value_token())) { + *schema << token; + } + + lex < css_whitespace >(); + + schema->rtrim(); + + return schema; + } + Warning* Parser::parse_warning() { if (stack.back() != Scope::Root && @@ -2286,6 +2451,7 @@ namespace Sass { // check expected opening bracket // only after successfull matching if (peek < exactly<'{'> >(q)) rv.found = q; + else if (peek < exactly<'('> >(q)) rv.found = q; // else if (peek < exactly<';'> >(q)) rv.found = q; // else if (peek < exactly<'}'> >(q)) rv.found = q; if (rv.found || *p == 0) rv.error = 0; diff --git a/src/parser.hpp b/src/parser.hpp index db4dc84f9b..e9284978d4 100644 --- a/src/parser.hpp +++ b/src/parser.hpp @@ -133,6 +133,8 @@ namespace Sass { const char* lex(bool lazy = true, bool force = false) { + if (*position == 0) return 0; + // position considered before lexed token // we can skip whitespace or comments for // lazy developers (but we need control) @@ -299,11 +301,21 @@ namespace Sass { Supports_Condition* parse_supports_condition_in_parens(); At_Root_Block* parse_at_root_block(); At_Root_Query* parse_at_root_query(); + String_Schema* parse_almost_any_value(); Directive* parse_special_directive(); + Directive* parse_prefixed_directive(); + Directive* parse_directive(); Warning* parse_warning(); Error* parse_error(); Debug* parse_debug(); + // be more like ruby sass + Expression* lex_almost_any_value_token(); + Expression* lex_almost_any_value_chars(); + Expression* lex_interp_string(); + Expression* lex_interp_uri(); + Expression* lex_interpolation(); + // these will throw errors Token lex_variable(); Token lex_identifier(); @@ -319,6 +331,34 @@ namespace Sass { void throw_syntax_error(std::string message, size_t ln = 0); void throw_read_error(std::string message, size_t ln = 0); + + + template + Expression* lex_interp() + { + if (lex < open >(false)) { + String_Schema* schema = SASS_MEMORY_NEW(ctx.mem, String_Schema, pstate); + // std::cerr << "LEX [[" << std::string(lexed) << "]]\n"; + *schema << SASS_MEMORY_NEW(ctx.mem, String_Constant, pstate, lexed); + if (position[0] == '#' && position[1] == '{') { + Expression* itpl = lex_interpolation(); + if (itpl) *schema << itpl; + while (lex < close >(false)) { + // std::cerr << "LEX [[" << std::string(lexed) << "]]\n"; + *schema << SASS_MEMORY_NEW(ctx.mem, String_Constant, pstate, lexed); + if (position[0] == '#' && position[1] == '{') { + Expression* itpl = lex_interpolation(); + if (itpl) *schema << itpl; + } else { + return schema; + } + } + } else { + return SASS_MEMORY_NEW(ctx.mem, String_Constant, pstate, lexed); + } + } + return 0; + } }; size_t check_bom_chars(const char* src, const char *end, const unsigned char* bom, size_t len); diff --git a/src/prelexer.cpp b/src/prelexer.cpp index 4b9069b8f9..ef68ab5a3a 100644 --- a/src/prelexer.cpp +++ b/src/prelexer.cpp @@ -16,6 +16,248 @@ namespace Sass { namespace Prelexer { + /* + + def string_re(open, close) + /#{open}((?:\\.|\#(?!\{)|[^#{close}\\#])*)(#{close}|#\{)/m + end + end + + # A hash of regular expressions that are used for tokenizing strings. + # + # The key is a `[Symbol, Boolean]` pair. + # The symbol represents which style of quotation to use, + # while the boolean represents whether or not the string + # is following an interpolated segment. + STRING_REGULAR_EXPRESSIONS = { + :double => { + /#{open}((?:\\.|\#(?!\{)|[^#{close}\\#])*)(#{close}|#\{)/m + false => string_re('"', '"'), + true => string_re('', '"') + }, + :single => { + false => string_re("'", "'"), + true => string_re('', "'") + }, + :uri => { + false => /url\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, + true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ + }, + # Defined in https://developer.mozilla.org/en/CSS/@-moz-document as a + # non-standard version of http://www.w3.org/TR/css3-conditional/ + :url_prefix => { + false => /url-prefix\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, + true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ + }, + :domain => { + false => /domain\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, + true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ + } + } + */ + + /* + /#{open} + ( + \\. + | + \# (?!\{) + | + [^#{close}\\#] + )* + (#{close}|#\{) + /m + false => string_re('"', '"'), + true => string_re('', '"') + */ + extern const char string_double_negates[] = "\"\\#"; + const char* re_string_double_close(const char* src) + { + return sequence < + // valid chars + zero_plus < + alternatives < + // escaped char + sequence < + exactly <'\\'>, + any_char + >, + // non interpolate hash + sequence < + exactly <'#'>, + negate < + exactly <'{'> + > + >, + // other valid chars + neg_class_char < + string_double_negates + > + > + >, + // quoted string closer + // or interpolate opening + alternatives < + exactly <'"'>, + lookahead < exactly< hash_lbrace > > + > + >(src); + } + + const char* re_string_double_open(const char* src) + { + return sequence < + // quoted string opener + exactly <'"'>, + // valid chars + zero_plus < + alternatives < + // escaped char + sequence < + exactly <'\\'>, + any_char + >, + // non interpolate hash + sequence < + exactly <'#'>, + negate < + exactly <'{'> + > + >, + // other valid chars + neg_class_char < + string_double_negates + > + > + >, + // quoted string closer + // or interpolate opening + alternatives < + exactly <'"'>, + lookahead < exactly< hash_lbrace > > + > + >(src); + } + + extern const char string_single_negates[] = "'\\#"; + const char* re_string_single_close(const char* src) + { + return sequence < + // valid chars + zero_plus < + alternatives < + // escaped char + sequence < + exactly <'\\'>, + any_char + >, + // non interpolate hash + sequence < + exactly <'#'>, + negate < + exactly <'{'> + > + >, + // other valid chars + neg_class_char < + string_single_negates + > + > + >, + // quoted string closer + // or interpolate opening + alternatives < + exactly <'\''>, + lookahead < exactly< hash_lbrace > > + > + >(src); + } + + const char* re_string_single_open(const char* src) + { + return sequence < + // quoted string opener + exactly <'\''>, + // valid chars + zero_plus < + alternatives < + // escaped char + sequence < + exactly <'\\'>, + any_char + >, + // non interpolate hash + sequence < + exactly <'#'>, + negate < + exactly <'{'> + > + >, + // other valid chars + neg_class_char < + string_single_negates + > + > + >, + // quoted string closer + // or interpolate opening + alternatives < + exactly <'\''>, + lookahead < exactly< hash_lbrace > > + > + >(src); + } + + /* + :uri => { + false => /url\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, + true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ + }, + */ + const char* re_string_uri_close(const char* src) + { + return sequence < + non_greedy< + alternatives< + class_char< real_uri_chars >, + uri_character, + NONASCII, + ESCAPE + >, + alternatives< + sequence < optional < W >, exactly <')'> >, + lookahead < exactly< hash_lbrace > > + > + >, + optional < + sequence < optional < W >, exactly <')'> > + > + >(src); + } + + const char* re_string_uri_open(const char* src) + { + return sequence < + exactly <'u'>, + exactly <'r'>, + exactly <'l'>, + exactly <'('>, + W, + non_greedy< + alternatives< + class_char< real_uri_chars >, + uri_character, + NONASCII, + ESCAPE + >, + alternatives< + sequence < W, exactly <')'> >, + exactly< hash_lbrace > + > + > + >(src); + } + // Match a line comment (/.*?(?=\n|\r\n?|\Z)/. const char* line_comment(const char* src) { @@ -366,10 +608,137 @@ namespace Sass { } // Match CSS '@' keywords. - const char* re_special_directive(const char* src) { + const char* at_keyword(const char* src) { return sequence, identifier>(src); } + /* + tok(%r{ + ( + \\. + | + (?!url\() + [^"'/\#!;\{\}] # " + | + /(?![\*\/]) + | + \#(?!\{) + | + !(?![a-z]) # TODO: never consume "!" when issue 1126 is fixed. + )+ + }xi) || tok(COMMENT) || tok(SINGLE_LINE_COMMENT) || interp_string || interp_uri || + interpolation(:warn_for_color) + */ + const char* re_almost_any_value_token(const char* src) { + + return alternatives < + one_plus < + alternatives < + sequence < + exactly <'\\'>, + any_char + >, + sequence < + negate < + sequence < + exactly < url_kwd >, + exactly <'('> + > + >, + neg_class_char < + almost_any_value_class + > + >, + sequence < + exactly <'/'>, + negate < + alternatives < + exactly <'/'>, + exactly <'*'> + > + > + >, + sequence < + exactly <'\\'>, + exactly <'#'>, + negate < + exactly <'{'> + > + >, + sequence < + exactly <'!'>, + negate < + alpha + > + > + > + >, + block_comment, + line_comment, + interpolant, + space, + sequence < + exactly<'u'>, + exactly<'r'>, + exactly<'l'>, + exactly<'('>, + zero_plus < + alternatives < + class_char< real_uri_chars >, + uri_character, + NONASCII, + ESCAPE + > + >, + // false => /url\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, + // true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ + exactly<')'> + > + >(src); + } + + /* + DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for, + :each, :while, :if, :else, :extend, :import, :media, :charset, :content, + :_moz_document, :at_root, :error] + */ + const char* re_special_directive(const char* src) { + return alternatives < + word < mixin_kwd >, + word < include_kwd >, + word < function_kwd >, + word < return_kwd >, + word < debug_kwd >, + word < warn_kwd >, + word < for_kwd >, + word < each_kwd >, + word < while_kwd >, + word < if_kwd >, + word < else_kwd >, + word < extend_kwd >, + word < import_kwd >, + word < media_kwd >, + word < charset_kwd >, + word < content_kwd >, + // exactly < moz_document_kwd >, + word < at_root_kwd >, + word < error_kwd > + >(src); + } + + const char* re_prefixed_directive(const char* src) { + return sequence < + optional < + sequence < + exactly <'-'>, + one_plus < alnum >, + exactly <'-'> + > + >, + exactly < supports_kwd > + >(src); + } + const char* re_reference_combinator(const char* src) { return sequence < optional < diff --git a/src/prelexer.hpp b/src/prelexer.hpp index 13bc2e21dd..69c4043237 100644 --- a/src/prelexer.hpp +++ b/src/prelexer.hpp @@ -162,6 +162,14 @@ namespace Sass { return src; } + // equivalent of STRING_REGULAR_EXPRESSIONS + const char* re_string_double_open(const char* src); + const char* re_string_double_close(const char* src); + const char* re_string_single_open(const char* src); + const char* re_string_single_close(const char* src); + const char* re_string_uri_open(const char* src); + const char* re_string_uri_close(const char* src); + // Match a line comment. const char* line_comment(const char* src); @@ -216,8 +224,13 @@ namespace Sass { // const char* url_schema(const char* src); // const char* url_value(const char* src); const char* vendor_prefix(const char* src); - // Match CSS '@' keywords. + const char* re_special_directive(const char* src); + const char* re_prefixed_directive(const char* src); + const char* re_almost_any_value_token(const char* src); + + // Match CSS '@' keywords. + const char* at_keyword(const char* src); const char* kwd_import(const char* src); const char* kwd_at_root(const char* src); const char* kwd_with_directive(const char* src);