# -*- coding: utf-8 -*- require 'set' module Sass module SCSS # The parser for SCSS. # It parses a string of code into a tree of {Sass::Tree::Node}s. class Parser # Expose for the SASS parser. attr_accessor :offset # @param str [String, StringScanner] The source document to parse. # Note that `Parser` *won't* raise a nice error message if this isn't properly parsed; # for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}. # @param filename [String] The name of the file being parsed. Used for # warnings and source maps. # @param importer [Sass::Importers::Base] The importer used to import the # file being parsed. Used for source maps. # @param line [Integer] The 1-based line on which the source string appeared, # if it's part of another document. # @param offset [Integer] The 1-based character (not byte) offset in the line on # which the source string starts. Used for error reporting and sourcemap # building. def initialize(str, filename, importer, line = 1, offset = 1) @template = str @filename = filename @importer = importer @line = line @offset = offset @strs = [] @expected = nil @throw_error = false end # Parses an SCSS document. # # @return [Sass::Tree::RootNode] The root node of the document tree # @raise [Sass::SyntaxError] if there's a syntax error in the document def parse init_scanner! root = stylesheet expected("selector or at-rule") unless root && @scanner.eos? root end # Parses an identifier with interpolation. # Note that this won't assert that the identifier takes up the entire input string; # it's meant to be used with `StringScanner`s as part of other parsers. # # @return [Array, nil] # The interpolated identifier, or nil if none could be parsed def parse_interp_ident init_scanner! interp_ident end # Parses a supports clause for an @import directive def parse_supports_clause init_scanner! ss clause = supports_clause ss clause end # Parses a media query list. # # @return [Sass::Media::QueryList] The parsed query list # @raise [Sass::SyntaxError] if there's a syntax error in the query list, # or if it doesn't take up the entire input string. def parse_media_query_list init_scanner! ql = media_query_list expected("media query list") unless ql && @scanner.eos? ql end # Parses an at-root query. # # @return [Array] The interpolated query. # @raise [Sass::SyntaxError] if there's a syntax error in the query, # or if it doesn't take up the entire input string. def parse_at_root_query init_scanner! query = at_root_query expected("@at-root query list") unless query && @scanner.eos? query end # Parses a supports query condition. # # @return [Sass::Supports::Condition] The parsed condition # @raise [Sass::SyntaxError] if there's a syntax error in the condition, # or if it doesn't take up the entire input string. def parse_supports_condition init_scanner! condition = supports_condition expected("supports condition") unless condition && @scanner.eos? condition end # Parses a custom property value. # # @return [Array] The interpolated value. # @raise [Sass::SyntaxError] if there's a syntax error in the value, # or if it doesn't take up the entire input string. def parse_declaration_value init_scanner! value = declaration_value expected('"}"') unless value && @scanner.eos? value end private include Sass::SCSS::RX def source_position Sass::Source::Position.new(@line, @offset) end def range(start_pos, end_pos = source_position) Sass::Source::Range.new(start_pos, end_pos, @filename, @importer) end def init_scanner! @scanner = if @template.is_a?(StringScanner) @template else Sass::Util::MultibyteStringScanner.new(@template.tr("\r", "")) end end def stylesheet node = node(Sass::Tree::RootNode.new(@scanner.string), source_position) block_contents(node, :stylesheet) {s(node)} end def s(node) while tok(S) || tok(CDC) || tok(CDO) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT)) next unless c process_comment c, node c = nil end true end def ss nil while tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT) true end def ss_comments(node) while tok(S) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT)) next unless c process_comment c, node c = nil end true end def whitespace return unless tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT) ss end def process_comment(text, node) silent = text =~ %r{\A//} loud = !silent && text =~ %r{\A/[/*]!} line = @line - text.count("\n") comment_start = @scanner.pos - text.length index_before_line = @scanner.string.rindex("\n", comment_start) || -1 offset = comment_start - index_before_line if silent value = [text.sub(%r{\A\s*//}, '/*').gsub(%r{^\s*//}, ' *') + ' */'] else value = Sass::Engine.parse_interp(text, line, offset, :filename => @filename) line_before_comment = @scanner.string[index_before_line + 1...comment_start] value.unshift(line_before_comment.gsub(/[^\s]/, ' ')) end type = if silent :silent elsif loud :loud else :normal end start_pos = Sass::Source::Position.new(line, offset) comment = node(Sass::Tree::CommentNode.new(value, type), start_pos) node << comment end DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for, :each, :while, :if, :else, :extend, :import, :media, :charset, :content, :_moz_document, :at_root, :error] PREFIXED_DIRECTIVES = Set[:supports] def directive start_pos = source_position return unless tok(/@/) name = tok!(IDENT) ss if (dir = special_directive(name, start_pos)) return dir elsif (dir = prefixed_directive(name, start_pos)) return dir end val = almost_any_value val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"] directive_body(val, start_pos) end def directive_body(value, start_pos) node = Sass::Tree::DirectiveNode.new(value) if tok(/\{/) node.has_children = true block_contents(node, :directive) tok!(/\}/) end node(node, start_pos) end def special_directive(name, start_pos) sym = name.tr('-', '_').to_sym DIRECTIVES.include?(sym) && send("#{sym}_directive", start_pos) end def prefixed_directive(name, start_pos) sym = deprefix(name).tr('-', '_').to_sym PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name, start_pos) end def mixin_directive(start_pos) name = tok! IDENT args, splat = sass_script(:parse_mixin_definition_arglist) ss block(node(Sass::Tree::MixinDefNode.new(name, args, splat), start_pos), :directive) end def include_directive(start_pos) name = tok! IDENT args, keywords, splat, kwarg_splat = sass_script(:parse_mixin_include_arglist) ss include_node = node( Sass::Tree::MixinNode.new(name, args, keywords, splat, kwarg_splat), start_pos) if tok?(/\{/) include_node.has_children = true block(include_node, :directive) else include_node end end def content_directive(start_pos) ss node(Sass::Tree::ContentNode.new, start_pos) end def function_directive(start_pos) name = tok! IDENT args, splat = sass_script(:parse_function_definition_arglist) ss block(node(Sass::Tree::FunctionNode.new(name, args, splat), start_pos), :function) end def return_directive(start_pos) node(Sass::Tree::ReturnNode.new(sass_script(:parse)), start_pos) end def debug_directive(start_pos) node(Sass::Tree::DebugNode.new(sass_script(:parse)), start_pos) end def warn_directive(start_pos) node(Sass::Tree::WarnNode.new(sass_script(:parse)), start_pos) end def for_directive(start_pos) tok!(/\$/) var = tok! IDENT ss tok!(/from/) from = sass_script(:parse_until, Set["to", "through"]) ss @expected = '"to" or "through"' exclusive = (tok(/to/) || tok!(/through/)) == 'to' to = sass_script(:parse) ss block(node(Sass::Tree::ForNode.new(var, from, to, exclusive), start_pos), :directive) end def each_directive(start_pos) tok!(/\$/) vars = [tok!(IDENT)] ss while tok(/,/) ss tok!(/\$/) vars << tok!(IDENT) ss end tok!(/in/) list = sass_script(:parse) ss block(node(Sass::Tree::EachNode.new(vars, list), start_pos), :directive) end def while_directive(start_pos) expr = sass_script(:parse) ss block(node(Sass::Tree::WhileNode.new(expr), start_pos), :directive) end def if_directive(start_pos) expr = sass_script(:parse) ss node = block(node(Sass::Tree::IfNode.new(expr), start_pos), :directive) pos = @scanner.pos line = @line ss else_block(node) || begin # Backtrack in case there are any comments we want to parse @scanner.pos = pos @line = line node end end def else_block(node) start_pos = source_position return unless tok(/@else/) ss else_node = block( node(Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), start_pos), :directive) node.add_else(else_node) pos = @scanner.pos line = @line ss else_block(node) || begin # Backtrack in case there are any comments we want to parse @scanner.pos = pos @line = line node end end def else_directive(start_pos) err("Invalid CSS: @else must come after @if") end def extend_directive(start_pos) selector_start_pos = source_position @expected = "selector" selector = Sass::Util.strip_string_array(expr!(:almost_any_value)) optional = tok(OPTIONAL) ss node(Sass::Tree::ExtendNode.new(selector, !!optional, range(selector_start_pos)), start_pos) end def import_directive(start_pos) values = [] loop do values << expr!(:import_arg) break if use_css_import? break unless tok(/,/) ss end values end def import_arg start_pos = source_position return unless (str = string) || (uri = tok?(/url\(/i)) if uri str = sass_script(:parse_string) ss supports = supports_clause ss media = media_query_list ss return node(Tree::CssImportNode.new(str, media.to_a, supports), start_pos) end ss supports = supports_clause ss media = media_query_list if str =~ %r{^(https?:)?//} || media || supports || use_css_import? return node( Sass::Tree::CssImportNode.new( Sass::Script::Value::String.quote(str), media.to_a, supports), start_pos) end node(Sass::Tree::ImportNode.new(str.strip), start_pos) end def use_css_import?; false; end def media_directive(start_pos) block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a), start_pos), :directive) end # http://www.w3.org/TR/css3-mediaqueries/#syntax def media_query_list query = media_query return unless query queries = [query] ss while tok(/,/) ss; queries << expr!(:media_query) end ss Sass::Media::QueryList.new(queries) end def media_query if (ident1 = interp_ident) ss ident2 = interp_ident ss if ident2 && ident2.length == 1 && ident2[0].is_a?(String) && ident2[0].downcase == 'and' query = Sass::Media::Query.new([], ident1, []) else if ident2 query = Sass::Media::Query.new(ident1, ident2, []) else query = Sass::Media::Query.new([], ident1, []) end return query unless tok(/and/i) ss end end if query expr = expr!(:media_expr) else expr = media_expr return unless expr end query ||= Sass::Media::Query.new([], [], []) query.expressions << expr ss while tok(/and/i) ss; query.expressions << expr!(:media_expr) end query end def query_expr interp = interpolation return interp if interp return unless tok(/\(/) res = ['('] ss res << sass_script(:parse) if tok(/:/) res << ': ' ss res << sass_script(:parse) end res << tok!(/\)/) ss res end # Aliases allow us to use different descriptions if the same # expression fails in different contexts. alias_method :media_expr, :query_expr alias_method :at_root_query, :query_expr def charset_directive(start_pos) name = expr!(:string) ss node(Sass::Tree::CharsetNode.new(name), start_pos) end # The document directive is specified in # http://www.w3.org/TR/css3-conditional/, but Gecko allows the # `url-prefix` and `domain` functions to omit quotation marks, contrary to # the standard. # # We could parse all document directives according to Mozilla's syntax, # but if someone's using e.g. @-webkit-document we don't want them to # think WebKit works sans quotes. def _moz_document_directive(start_pos) res = ["@-moz-document "] loop do res << str {ss} << expr!(:moz_document_function) if (c = tok(/,/)) res << c else break end end directive_body(res.flatten, start_pos) end def moz_document_function val = interp_uri || _interp_string(:url_prefix) || _interp_string(:domain) || function(false) || interpolation return unless val ss val end def at_root_directive(start_pos) if tok?(/\(/) && (expr = at_root_query) return block(node(Sass::Tree::AtRootNode.new(expr), start_pos), :directive) end at_root_node = node(Sass::Tree::AtRootNode.new, start_pos) rule_node = ruleset return block(at_root_node, :stylesheet) unless rule_node at_root_node << rule_node at_root_node end def at_root_directive_list return unless (first = tok(IDENT)) arr = [first] ss while (e = tok(IDENT)) arr << e ss end arr end def error_directive(start_pos) node(Sass::Tree::ErrorNode.new(sass_script(:parse)), start_pos) end # http://www.w3.org/TR/css3-conditional/ def supports_directive(name, start_pos) condition = expr!(:supports_condition) node = Sass::Tree::SupportsNode.new(name, condition) tok!(/\{/) node.has_children = true block_contents(node, :directive) tok!(/\}/) node(node, start_pos) end def supports_clause return unless tok(/supports\(/i) ss supports = import_supports_condition ss tok!(/\)/) supports end def supports_condition supports_negation || supports_operator || supports_interpolation end def import_supports_condition supports_condition || supports_declaration end def supports_negation return unless tok(/not/i) ss Sass::Supports::Negation.new(expr!(:supports_condition_in_parens)) end def supports_operator cond = supports_condition_in_parens return unless cond re = /and|or/i while (op = tok(re)) re = /#{op}/i ss cond = Sass::Supports::Operator.new( cond, expr!(:supports_condition_in_parens), op) end cond end def supports_declaration name = sass_script(:parse) tok!(/:/); ss value = sass_script(:parse) Sass::Supports::Declaration.new(name, value) end def supports_condition_in_parens interp = supports_interpolation return interp if interp return unless tok(/\(/); ss if (cond = supports_condition) tok!(/\)/); ss cond else decl = supports_declaration tok!(/\)/); ss decl end end def supports_interpolation interp = interpolation return unless interp ss Sass::Supports::Interpolation.new(interp) end def variable return unless tok(/\$/) start_pos = source_position name = tok!(IDENT) ss; tok!(/:/); ss expr = sass_script(:parse) while tok(/!/) flag_name = tok!(IDENT) if flag_name == 'default' guarded ||= true elsif flag_name == 'global' global ||= true else raise Sass::SyntaxError.new("Invalid flag \"!#{flag_name}\".", :line => @line) end ss end result = Sass::Tree::VariableNode.new(name, expr, guarded, global) node(result, start_pos) end def operator # Many of these operators (all except / and ,) # are disallowed by the CSS spec, # but they're included here for compatibility # with some proprietary MS properties str {ss if tok(%r{[/,:.=]})} end def ruleset start_pos = source_position return unless (rules = almost_any_value) block( node( Sass::Tree::RuleNode.new(rules, range(start_pos)), start_pos), :ruleset) end def block(node, context) node.has_children = true tok!(/\{/) block_contents(node, context) tok!(/\}/) node end # A block may contain declarations and/or rulesets def block_contents(node, context) block_given? ? yield : ss_comments(node) node << (child = block_child(context)) while tok(/;/) || has_children?(child) block_given? ? yield : ss_comments(node) node << (child = block_child(context)) end node end def block_child(context) return variable || directive if context == :function return variable || directive || ruleset if context == :stylesheet variable || directive || declaration_or_ruleset end def has_children?(child_or_array) return false unless child_or_array return child_or_array.last.has_children if child_or_array.is_a?(Array) child_or_array.has_children end # When parsing the contents of a ruleset, it can be difficult to tell # declarations apart from nested rulesets. Since we don't thoroughly parse # selectors until after resolving interpolation, we can share a bunch of # the parsing of the two, but we need to disambiguate them first. We use # the following criteria: # # * If the entity doesn't start with an identifier followed by a colon, # it's a selector. There are some additional mostly-unimportant cases # here to support various declaration hacks. # # * If the colon is followed by another colon, it's a selector. # # * Otherwise, if the colon is followed by anything other than # interpolation or a character that's valid as the beginning of an # identifier, it's a declaration. # # * If the colon is followed by interpolation or a valid identifier, try # parsing it as a declaration value. If this fails, backtrack and parse # it as a selector. # # * If the declaration value value valid but is followed by "{", backtrack # and parse it as a selector anyway. This ensures that ".foo:bar {" is # always parsed as a selector and never as a property with nested # properties beneath it. def declaration_or_ruleset start_pos = source_position declaration = try_declaration if declaration.nil? return unless (selector = almost_any_value) elsif declaration.is_a?(Array) selector = declaration else # Declaration should be a PropNode. return declaration end if (additional_selector = almost_any_value) selector << additional_selector end block( node( Sass::Tree::RuleNode.new(merge(selector), range(start_pos)), start_pos), :ruleset) end # Tries to parse a declaration, and returns the value parsed so far if it # fails. # # This has three possible return types. It can return `nil`, indicating # that parsing failed completely and the scanner hasn't moved forward at # all. It can return an Array, indicating that parsing failed after # consuming some text (possibly containing interpolation), which is # returned. Or it can return a PropNode, indicating that parsing # succeeded. def try_declaration # This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop: # val" hacks. name_start_pos = source_position if (s = tok(/[:\*\.]|\#(?!\{)/)) name = [s, str {ss}] return name unless (ident = interp_ident) name << ident else return unless (name = interp_ident) name = Array(name) end if (comment = tok(COMMENT)) name << comment end name_end_pos = source_position mid = [str {ss}] return name + mid unless tok(/:/) mid << ':' # If this is a CSS variable, parse it as a property no matter what. if name.first.is_a?(String) && name.first.start_with?("--") return css_variable_declaration(name, name_start_pos, name_end_pos) end return name + mid + [':'] if tok(/:/) mid << str {ss} post_colon_whitespace = !mid.last.empty? could_be_selector = !post_colon_whitespace && (tok?(IDENT_START) || tok?(INTERP_START)) value_start_pos = source_position value = nil error = catch_error do value = value! if tok?(/\{/) # Properties that are ambiguous with selectors can't have additional # properties nested beneath them. tok!(/;/) if could_be_selector elsif !tok?(/[;{}]/) # We want an exception if there's no valid end-of-property character # exists, but we don't want to consume it if it does. tok!(/[;{}]/) end end if error rethrow error unless could_be_selector # If the value would be followed by a semicolon, it's definitely # supposed to be a property, not a selector. additional_selector = almost_any_value rethrow error if tok?(/;/) return name + mid + (additional_selector || []) end value_end_pos = source_position ss require_block = tok?(/\{/) node = node(Sass::Tree::PropNode.new(name.flatten.compact, [value], :new), name_start_pos, value_end_pos) node.name_source_range = range(name_start_pos, name_end_pos) node.value_source_range = range(value_start_pos, value_end_pos) return node unless require_block nested_properties! node end def css_variable_declaration(name, name_start_pos, name_end_pos) value_start_pos = source_position value = declaration_value value_end_pos = source_position node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new), name_start_pos, value_end_pos) node.name_source_range = range(name_start_pos, name_end_pos) node.value_source_range = range(value_start_pos, value_end_pos) node end # This production consumes values that could be a selector, an expression, # or a combination of both. It respects strings and comments and supports # interpolation. It will consume up to "{", "}", ";", or "!". # # Values consumed by this production will usually be parsed more # thoroughly once interpolation has been resolved. def almost_any_value return unless (tok = almost_any_value_token) sel = [tok] while (tok = almost_any_value_token) sel << tok end merge(sel) end def almost_any_value_token tok(%r{ ( \\. | (?!url\() [^"'/\#!;\{\}] # " | # interp_uri will handle most url() calls, but not ones that take strings url\(#{W}(?=") | /(?![/*]) | \#(?!\{) | !(?![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) end def declaration_value(top_level: true) return unless (tok = declaration_value_token(top_level)) value = [tok] while (tok = declaration_value_token(top_level)) value << tok end merge(value) end def declaration_value_token(top_level) # This comes, more or less, from the [token consumption algorithm][]. # However, since we don't have to worry about the token semantics, we # just consume everything until we come across a token with special # semantics. # # [token consumption algorithm]: https://drafts.csswg.org/css-syntax-3/#consume-token. result = tok(%r{ ( (?! url\( ) [^()\[\]{}"'#/ \t\r\n\f#{top_level ? ";" : ""}] | \#(?!\{) | /(?!\*) )+ }xi) || interp_string || interp_uri || interpolation || tok(COMMENT) return result if result # Fold together multiple characters of whitespace that don't include # newlines. The value only cares about the tokenization, so this is safe # as long as we don't delete whitespace entirely. It's important that we # fold here rather than post-processing, since we aren't allowed to fold # whitespace within strings and we lose that context later on. if (ws = tok(S)) return ws.include?("\n") ? ws.gsub(/\A[^\n]*/, '') : ' ' end if tok(/\(/) value = declaration_value(top_level: false) tok!(/\)/) ['(', *value, ')'] elsif tok(/\[/) value = declaration_value(top_level: false) tok!(/\]/) ['[', *value, ']'] elsif tok(/\{/) value = declaration_value(top_level: false) tok!(/\}/) ['{', *value, '}'] end end def declaration # This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop: # val" hacks. name_start_pos = source_position if (s = tok(/[:\*\.]|\#(?!\{)/)) name = [s, str {ss}, *expr!(:interp_ident)] else return unless (name = interp_ident) name = Array(name) end if (comment = tok(COMMENT)) name << comment end name_end_pos = source_position ss tok!(/:/) ss value_start_pos = source_position value = value! value_end_pos = source_position ss require_block = tok?(/\{/) node = node(Sass::Tree::PropNode.new(name.flatten.compact, [value], :new), name_start_pos, value_end_pos) node.name_source_range = range(name_start_pos, name_end_pos) node.value_source_range = range(value_start_pos, value_end_pos) return node unless require_block nested_properties! node end def value! if tok?(/\{/) str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new("")) str.line = source_position.line str.source_range = range(source_position) return str end start_pos = source_position # This is a bit of a dirty trick: # if the value is completely static, # we don't parse it at all, and instead return a plain old string # containing the value. # This results in a dramatic speed increase. if (val = tok(STATIC_VALUE)) str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(val.strip)) str.line = start_pos.line str.source_range = range(start_pos) return str end sass_script(:parse) end def nested_properties!(node) @expected = 'expression (e.g. 1px, bold) or "{"' block(node, :property) end def expr(allow_var = true) t = term(allow_var) return unless t res = [t, str {ss}] while (o = operator) && (t = term(allow_var)) res << o << t << str {ss} end res.flatten end def term(allow_var) e = tok(NUMBER) || interp_uri || function(allow_var) || interp_string || tok(UNICODERANGE) || interp_ident || tok(HEXCOLOR) || (allow_var && var_expr) return e if e op = tok(/[+-]/) return unless op @expected = "number or function" [op, tok(NUMBER) || function(allow_var) || (allow_var && var_expr) || expr!(:interpolation)] end def function(allow_var) name = tok(FUNCTION) return unless name if name == "expression(" || name == "calc(" str, _ = Sass::Shared.balance(@scanner, ?(, ?), 1) [name, str] else [name, str {ss}, expr(allow_var), tok!(/\)/)] end end def var_expr return unless tok(/\$/) line = @line var = Sass::Script::Tree::Variable.new(tok!(IDENT)) var.line = line var end def interpolation(warn_for_color = false) return unless tok(INTERP_START) sass_script(:parse_interpolated, warn_for_color) end def string return unless tok(STRING) Sass::Script::Value::String.value(@scanner[1] || @scanner[2]) end def interp_string _interp_string(:double) || _interp_string(:single) end def interp_uri _interp_string(:uri) end def _interp_string(type) start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][false]) return unless start res = [start] mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][true] # @scanner[2].empty? means we've started an interpolated section while @scanner[2] == '#{' @scanner.pos -= 2 # Don't consume the #{ res.last.slice!(-2..-1) res << expr!(:interpolation) << tok(mid_re) end res end def interp_ident(start = IDENT) val = tok(start) || interpolation(:warn_for_color) || tok(IDENT_HYPHEN_INTERP) return unless val res = [val] while (val = tok(NAME) || interpolation(:warn_for_color)) res << val end res end def interp_ident_or_var id = interp_ident return id if id var = var_expr return [var] if var end def str @strs.push String.new("") yield @strs.last ensure @strs.pop end def str? pos = @scanner.pos line = @line offset = @offset @strs.push "" throw_error {yield} && @strs.last rescue Sass::SyntaxError @scanner.pos = pos @line = line @offset = offset nil ensure @strs.pop end def node(node, start_pos, end_pos = source_position) node.line = start_pos.line node.source_range = range(start_pos, end_pos) node end @sass_script_parser = Sass::Script::Parser class << self # @private attr_accessor :sass_script_parser end def sass_script(*args) parser = self.class.sass_script_parser.new(@scanner, @line, @offset, :filename => @filename, :importer => @importer, :allow_extra_text => true) result = parser.send(*args) unless @strs.empty? # Convert to CSS manually so that comments are ignored. src = result.to_sass @strs.each {|s| s << src} end @line = parser.line @offset = parser.offset result rescue Sass::SyntaxError => e throw(:_sass_parser_error, true) if @throw_error raise e end def merge(arr) arr && Sass::Util.merge_adjacent_strings([arr].flatten) end EXPR_NAMES = { :media_query => "media query (e.g. print, screen, print and screen)", :media_query_list => "media query (e.g. print, screen, print and screen)", :media_expr => "media expression (e.g. (min-device-width: 800px))", :at_root_query => "@at-root query (e.g. (without: media))", :at_root_directive_list => '* or identifier', :declaration_value => "expression (e.g. fr, 2n+1)", :interp_ident => "identifier", :qualified_name => "identifier", :expr => "expression (e.g. 1px, bold)", :selector_comma_sequence => "selector", :string => "string", :import_arg => "file to import (string or url())", :moz_document_function => "matching function (e.g. url-prefix(), domain())", :supports_condition => "@supports condition (e.g. (display: flexbox))", :supports_condition_in_parens => "@supports condition (e.g. (display: flexbox))", :a_n_plus_b => "An+B expression", :keyframes_selector_component => "from, to, or a percentage", :keyframes_selector => "keyframes selector (e.g. 10%)" } TOK_NAMES = Hash[Sass::SCSS::RX.constants.map do |c| [Sass::SCSS::RX.const_get(c), c.downcase] end].merge( IDENT => "identifier", /[;{}]/ => '";"', /\b(without|with)\b/ => '"with" or "without"' ) def tok?(rx) @scanner.match?(rx) end def expr!(name) e = send(name) return e if e expected(EXPR_NAMES[name] || name.to_s) end def tok!(rx) t = tok(rx) return t if t name = TOK_NAMES[rx] unless name # Display basic regexps as plain old strings source = rx.source.gsub(%r{\\/}, '/') string = rx.source.gsub(/\\(.)/, '\1') name = source == Regexp.escape(string) ? string.inspect : rx.inspect end expected(name) end def expected(name) throw(:_sass_parser_error, true) if @throw_error self.class.expected(@scanner, @expected || name, @line) end def err(msg) throw(:_sass_parser_error, true) if @throw_error raise Sass::SyntaxError.new(msg, :line => @line) end def throw_error old_throw_error, @throw_error = @throw_error, false yield ensure @throw_error = old_throw_error end def catch_error(&block) old_throw_error, @throw_error = @throw_error, true pos = @scanner.pos line = @line offset = @offset expected = @expected logger = Sass::Logger::Delayed.install! if catch(:_sass_parser_error) {yield; false} @scanner.pos = pos @line = line @offset = offset @expected = expected {:pos => pos, :line => line, :expected => @expected, :block => block} else logger.flush nil end ensure logger.uninstall! if logger @throw_error = old_throw_error end def rethrow(err) if @throw_error throw :_sass_parser_error, err else @scanner = Sass::Util::MultibyteStringScanner.new(@scanner.string) @scanner.pos = err[:pos] @line = err[:line] @expected = err[:expected] err[:block].call end end # @private def self.expected(scanner, expected, line) pos = scanner.pos after = scanner.string[0...pos] # Get rid of whitespace between pos and the last token, # but only if there's a newline in there after.gsub!(/\s*\n\s*$/, '') # Also get rid of stuff before the last newline after.gsub!(/.*\n/, '') after = "..." + after[-15..-1] if after.size > 18 was = scanner.rest.dup # Get rid of whitespace between pos and the next token, # but only if there's a newline in there was.gsub!(/^\s*\n\s*/, '') # Also get rid of stuff after the next newline was.gsub!(/\n.*/, '') was = was[0...15] + "..." if was.size > 18 raise Sass::SyntaxError.new( "Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"", :line => line) end # Avoid allocating lots of new strings for `#tok`. # This is important because `#tok` is called all the time. NEWLINE = "\n" def tok(rx) res = @scanner.scan(rx) return unless res newline_count = res.count(NEWLINE) if newline_count > 0 @line += newline_count @offset = res[res.rindex(NEWLINE)..-1].size else @offset += res.size end @expected = nil if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT @strs.each {|s| s << res} end res end # Remove a vendor prefix from `str`. def deprefix(str) str.gsub(/^-[a-zA-Z0-9]+-/, '') end end end end