require 'sass/script/css_parser' module Sass module SCSS # A parser for a static SCSS tree. # Parses with SCSS extensions, like nested rules and parent selectors, # but without dynamic SassScript. # This is useful for e.g. \{#parse\_selector parsing selectors} # after resolving the interpolation. class StaticParser < Parser # Parses the text as a selector. # # @param filename [String, nil] The file in which the selector appears, # or nil if there is no such file. # Used for error reporting. # @return [Selector::CommaSequence] The parsed selector # @raise [Sass::SyntaxError] if there's a syntax error in the selector def parse_selector init_scanner! seq = expr!(:selector_comma_sequence) expected("selector") unless @scanner.eos? seq.line = @line seq.filename = @filename seq end # Parses a static at-root query. # # @return [(Symbol, Array)] The type of the query # (`:with` or `:without`) and the values that are being filtered. # @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_static_at_root_query init_scanner! tok!(/\(/); ss type = tok!(/\b(without|with)\b/).to_sym; ss tok!(/:/); ss directives = expr!(:at_root_directive_list); ss tok!(/\)/) expected("@at-root query list") unless @scanner.eos? return type, directives end def parse_keyframes_selector init_scanner! sel = expr!(:keyframes_selector) expected("keyframes selector") unless @scanner.eos? sel end # @see Parser#initialize # @param allow_parent_ref [Boolean] Whether to allow the # parent-reference selector, `&`, when parsing the document. # @comment # rubocop:disable ParameterLists def initialize(str, filename, importer, line = 1, offset = 1, allow_parent_ref = true) # rubocop:enable ParameterLists super(str, filename, importer, line, offset) @allow_parent_ref = allow_parent_ref end private def moz_document_function val = tok(URI) || tok(URL_PREFIX) || tok(DOMAIN) || function(!:allow_var) return unless val ss [val] end def variable; nil; end def script_value; nil; end def interpolation(warn_for_color = false); nil; end def var_expr; nil; end def interp_string; (s = tok(STRING)) && [s]; end def interp_uri; (s = tok(URI)) && [s]; end def interp_ident(ident = IDENT); (s = tok(ident)) && [s]; end def use_css_import?; true; end def special_directive(name, start_pos) return unless %w[media import charset -moz-document].include?(name) super end def selector_comma_sequence sel = selector return unless sel selectors = [sel] ws = '' while tok(/,/) ws << str {ss} if (sel = selector) selectors << sel if ws.include?("\n") selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members) end ws = '' end end Selector::CommaSequence.new(selectors) end def selector_string sel = selector return unless sel sel.to_s end def selector start_pos = source_position # The combinator here allows the "> E" hack val = combinator || simple_selector_sequence return unless val nl = str {ss}.include?("\n") res = [] res << val res << "\n" if nl while (val = combinator || simple_selector_sequence) res << val res << "\n" if str {ss}.include?("\n") end seq = Selector::Sequence.new(res.compact) if seq.members.any? {|sseq| sseq.is_a?(Selector::SimpleSequence) && sseq.subject?} location = " of #{@filename}" if @filename Sass::Util.sass_warn < e e.message << "\n\n\"#{sel}\" may only be used at the beginning of a compound selector." raise e end end Selector::SimpleSequence.new(res, tok(/!/), range(start_pos)) end def parent_selector return unless @allow_parent_ref && tok(/&/) Selector::Parent.new(tok(NAME)) end def class_selector return unless tok(/\./) @expected = "class name" Selector::Class.new(tok!(IDENT)) end def id_selector return unless tok(/#(?!\{)/) @expected = "id name" Selector::Id.new(tok!(NAME)) end def placeholder_selector return unless tok(/%/) @expected = "placeholder name" Selector::Placeholder.new(tok!(IDENT)) end def element_name ns, name = Sass::Util.destructure(qualified_name(:allow_star_name)) return unless ns || name if name == '*' Selector::Universal.new(ns) else Selector::Element.new(name, ns) end end def qualified_name(allow_star_name = false) name = tok(IDENT) || tok(/\*/) || (tok?(/\|/) && "") return unless name return nil, name unless tok(/\|/) return name, tok!(IDENT) unless allow_star_name @expected = "identifier or *" return name, tok(IDENT) || tok!(/\*/) end def attrib return unless tok(/\[/) ss ns, name = attrib_name! ss op = tok(/=/) || tok(INCLUDES) || tok(DASHMATCH) || tok(PREFIXMATCH) || tok(SUFFIXMATCH) || tok(SUBSTRINGMATCH) if op @expected = "identifier or string" ss val = tok(IDENT) || tok!(STRING) ss end flags = tok(IDENT) || tok(STRING) tok!(/\]/) Selector::Attribute.new(name, ns, op, val, flags) end def attrib_name! if (name_or_ns = tok(IDENT)) # E, E|E if tok(/\|(?!=)/) ns = name_or_ns name = tok(IDENT) else name = name_or_ns end else # *|E or |E ns = tok(/\*/) || "" tok!(/\|/) name = tok!(IDENT) end return ns, name end SELECTOR_PSEUDO_CLASSES = %w[not matches current any has host host-context].to_set PREFIXED_SELECTOR_PSEUDO_CLASSES = %w[nth-child nth-last-child].to_set def pseudo s = tok(/::?/) return unless s @expected = "pseudoclass or pseudoelement" name = tok!(IDENT) if tok(/\(/) ss deprefixed = deprefix(name) if s == ':' && SELECTOR_PSEUDO_CLASSES.include?(deprefixed) sel = selector_comma_sequence elsif s == ':' && PREFIXED_SELECTOR_PSEUDO_CLASSES.include?(deprefixed) arg, sel = prefixed_selector_pseudo else arg = expr!(:pseudo_args) end tok!(/\)/) end Selector::Pseudo.new(s == ':' ? :class : :element, name, arg, sel) end def pseudo_args arg = expr!(:pseudo_expr) while tok(/,/) arg << ',' << str {ss} arg.concat expr!(:pseudo_expr) end arg end def pseudo_expr res = pseudo_expr_token return unless res res << str {ss} while (e = pseudo_expr_token) res << e << str {ss} end res end def pseudo_expr_token tok(PLUS) || tok(/[-*]/) || tok(NUMBER) || tok(STRING) || tok(IDENT) end def prefixed_selector_pseudo prefix = str do expr = str {expr!(:a_n_plus_b)} ss return expr, nil unless tok(/of/) ss end return prefix, expr!(:selector_comma_sequence) end def a_n_plus_b if (parity = tok(/even|odd/i)) return parity end if tok(/[+-]?[0-9]+/) ss return true unless tok(/n/) else return unless tok(/[+-]?n/i) end ss return true unless tok(/[+-]/) ss @expected = "number" tok!(/[0-9]+/) true end def keyframes_selector ss str do return unless keyframes_selector_component ss while tok(/,/) ss expr!(:keyframes_selector_component) ss end end end def keyframes_selector_component tok(/from|to/i) || tok(PERCENTAGE) end @sass_script_parser = Class.new(Sass::Script::CssParser) @sass_script_parser.send(:include, ScriptParser) end end end