haml-3.0.25.lib.sass.scss.parser.rb Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scalate-jruby_2.10 Show documentation
Show all versions of scalate-jruby_2.10 Show documentation
Scalate integration with JRuby to access Ruby based filters such as sass
The newest version!
require 'strscan'
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
# @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 line [Fixnum] The line on which the source string appeared,
# if it's part of another document
def initialize(str, line = 1)
@template = str
@line = line
@strs = []
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 @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
private
include Sass::SCSS::RX
def init_scanner!
@scanner =
if @template.is_a?(StringScanner)
@template
else
StringScanner.new(@template.gsub("\r", ""))
end
end
def stylesheet
node = node(Sass::Tree::RootNode.new(@scanner.string))
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)
single_line = text =~ /^\/\//
pre_str = single_line ? "" : @scanner.
string[[email protected]].
reverse[/.*?\*\/(.*?)($|\Z)/, 1].
reverse.gsub(/[^\s]/, ' ')
text = text.sub(/^\s*\/\//, '/*').gsub(/^\s*\/\//, ' *') + ' */' if single_line
comment = Sass::Tree::CommentNode.new(pre_str + text, single_line)
comment.line = @line - text.count("\n")
node << comment
end
DIRECTIVES = Set[:mixin, :include, :debug, :warn, :for, :while, :if, :else,
:extend, :import, :media, :charset]
def directive
return unless tok(/@/)
name = tok!(IDENT)
ss
if dir = special_directive(name)
return dir
end
# Most at-rules take expressions (e.g. @import),
# but some (e.g. @page) take selector-like arguments
val = str {break unless expr}
val ||= CssParser.new(@scanner, @line).parse_selector_string
node = node(Sass::Tree::DirectiveNode.new("@#{name} #{val}".strip))
if tok(/\{/)
node.has_children = true
block_contents(node, :directive)
tok!(/\}/)
end
node
end
def special_directive(name)
sym = name.gsub('-', '_').to_sym
DIRECTIVES.include?(sym) && send("#{sym}_directive")
end
def mixin_directive
name = tok! IDENT
args = sass_script(:parse_mixin_definition_arglist)
ss
block(node(Sass::Tree::MixinDefNode.new(name, args)), :directive)
end
def include_directive
name = tok! IDENT
args = sass_script(:parse_mixin_include_arglist)
ss
node(Sass::Tree::MixinNode.new(name, args))
end
def debug_directive
node(Sass::Tree::DebugNode.new(sass_script(:parse)))
end
def warn_directive
node(Sass::Tree::WarnNode.new(sass_script(:parse)))
end
def for_directive
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)), :directive)
end
def while_directive
expr = sass_script(:parse)
ss
block(node(Sass::Tree::WhileNode.new(expr)), :directive)
end
def if_directive
expr = sass_script(:parse)
ss
node = block(node(Sass::Tree::IfNode.new(expr)), :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)
return unless tok(/@else/)
ss
else_node = block(
Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))),
: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
raise Sass::SyntaxError.new(
"Invalid CSS: @else must come after @if", :line => @line)
end
def extend_directive
node(Sass::Tree::ExtendNode.new(expr!(:selector)))
end
def import_directive
values = []
loop do
values << expr!(:import_arg)
break if use_css_import? || !tok(/,\s*/)
end
return values
end
def import_arg
return unless arg = tok(STRING) || (uri = tok!(URI))
path = @scanner[1] || @scanner[2] || @scanner[3]
ss
media = str {media_query_list}.strip
if uri || path =~ /^http:\/\// || !media.strip.empty? || use_css_import?
return node(Sass::Tree::DirectiveNode.new("@import #{arg} #{media}".strip))
end
node(Sass::Tree::ImportNode.new(path.strip))
end
def use_css_import?; false; end
def media_directive
val = str {media_query_list}.strip
block(node(Sass::Tree::DirectiveNode.new("@media #{val}")), :directive)
end
# http://www.w3.org/TR/css3-mediaqueries/#syntax
def media_query_list
return unless media_query
ss
while tok(/,/)
ss; expr!(:media_query); ss
end
true
end
def media_query
if tok(/only|not/i)
ss
@expected = "media type (e.g. print, screen)"
tok!(IDENT)
ss
elsif !tok(IDENT) && !media_expr
return
end
ss
while tok(/and/i)
ss; expr!(:media_expr); ss
end
true
end
def media_expr
return unless tok(/\(/)
ss
@expected = "media feature (e.g. min-device-width, color)"
tok!(IDENT)
ss
if tok(/:/)
ss; expr!(:expr)
end
tok!(/\)/)
ss
true
end
def charset_directive
tok! STRING
name = @scanner[1] || @scanner[2]
ss
node(Sass::Tree::CharsetNode.new(name))
end
def variable
return unless tok(/\$/)
name = tok!(IDENT)
ss; tok!(/:/); ss
expr = sass_script(:parse)
guarded = tok(DEFAULT)
node(Sass::Tree::VariableNode.new(name, expr, guarded))
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(/[\/,:.=]/)}
end
def unary_operator
tok(/[+-]/)
end
def ruleset
return unless rules = selector_sequence
block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :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 || 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)
return child_or_array.has_children
end
# This is a nasty hack, and the only place in the parser
# that requires backtracking.
# The reason is that we can't figure out if certain strings
# are declarations or rulesets with fixed finite lookahead.
# For example, "foo:bar baz baz baz..." could be either a property
# or a selector.
#
# To handle this, we simply check if it works as a property
# (which is the most common case)
# and, if it doesn't, try it as a ruleset.
#
# We could eke some more efficiency out of this
# by handling some easy cases (first token isn't an identifier,
# no colon after the identifier, whitespace after the colon),
# but I'm not sure the gains would be worth the added complexity.
def declaration_or_ruleset
pos = @scanner.pos
line = @line
old_use_property_exception, @use_property_exception =
@use_property_exception, false
begin
decl = declaration
unless decl && decl.has_children
# We want an exception if it's not there,
# but we don't want to consume if it is
tok!(/[;}]/) unless tok?(/[;}]/)
end
return decl
rescue Sass::SyntaxError => decl_err
end
@line = line
@scanner.pos = pos
begin
return ruleset
rescue Sass::SyntaxError => ruleset_err
raise @use_property_exception ? decl_err : ruleset_err
end
ensure
@use_property_exception = old_use_property_exception
end
def selector_sequence
if sel = tok(STATIC_SELECTOR)
return [sel]
end
rules = []
return unless v = selector
rules.concat v
while tok(/,/)
rules << ',' << str {ss}
rules.concat expr!(:selector)
end
rules
end
def selector
return unless sel = _selector
sel.to_a
end
def selector_comma_sequence
return unless sel = _selector
selectors = [sel]
while tok(/,/)
ws = str{ss}
selectors << expr!(:_selector)
selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members) if ws.include?("\n")
end
Selector::CommaSequence.new(selectors)
end
def _selector
# The combinator here allows the "> E" hack
return unless val = combinator || simple_selector_sequence
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
Selector::Sequence.new(res.compact)
end
def combinator
tok(PLUS) || tok(GREATER) || tok(TILDE)
end
def simple_selector_sequence
# This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
return expr unless e = element_name || id_selector || class_selector ||
attrib || negation || pseudo || parent_selector || interpolation_selector
res = [e]
# The tok(/\*/) allows the "E*" hack
while v = element_name || id_selector || class_selector ||
attrib || negation || pseudo || interpolation_selector ||
(tok(/\*/) && Selector::Universal.new(nil))
res << v
end
if tok?(/&/)
begin
expected('"{"')
rescue Sass::SyntaxError => e
e.message << "\n\n" << < @line) unless space
Invalid CSS: a space is required between a property and its definition
when it has other properties nested beneath it.
MESSAGE
@use_property_exception = true
@expected = 'expression (e.g. 1px, bold) or "{"'
block(node, :property)
end
def expr
return unless t = term
res = [t, str{ss}]
while (o = operator) && (t = term)
res << o << t << str{ss}
end
res
end
def term
unless e = tok(NUMBER) ||
tok(URI) ||
function ||
tok(STRING) ||
tok(UNICODERANGE) ||
tok(IDENT) ||
tok(HEXCOLOR)
return unless op = unary_operator
@expected = "number or function"
return [op, tok(NUMBER) || expr!(:function)]
end
e
end
def function
return unless name = tok(FUNCTION)
if name == "expression(" || name == "calc("
str, _ = Haml::Shared.balance(@scanner, ?(, ?), 1)
[name, str]
else
[name, str{ss}, expr, tok!(/\)/)]
end
end
def interpolation
return unless tok(INTERP_START)
sass_script(:parse_interpolated)
end
def interp_string
_interp_string(:double) || _interp_string(:single)
end
def _interp_string(type)
return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, false]])
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)
return unless val = tok(start) || interpolation
res = [val]
while val = tok(NAME) || interpolation
res << val
end
res
end
def interp_name
interp_ident NAME
end
def str
@strs.push ""
yield
@strs.last
ensure
@strs.pop
end
def str?
@strs.push ""
yield && @strs.last
ensure
@strs.pop
end
def node(node)
node.line = @line
node
end
@sass_script_parser = Class.new(Sass::Script::Parser)
@sass_script_parser.send(:include, ScriptParser)
# @private
def self.sass_script_parser; @sass_script_parser; end
def sass_script(*args)
parser = self.class.sass_script_parser.new(@scanner, @line,
@scanner.pos - (@scanner.string[[email protected]].rindex("\n") || 0))
result = parser.send(*args)
@line = parser.line
result
end
def merge(arr)
arr && Haml::Util.merge_adjacent_strings([arr].flatten)
end
EXPR_NAMES = {
:media_query => "media query (e.g. print, screen, print and screen)",
:media_expr => "media expression (e.g. (min-device-width: 800px)))",
:pseudo_expr => "expression (e.g. fr, 2n+1)",
:interp_ident => "identifier",
:interp_name => "identifier",
:expr => "expression (e.g. 1px, bold)",
:selector_comma_sequence => "selector",
:simple_selector_sequence => "selector",
:import_arg => "file to import (string or url())",
}
TOK_NAMES = Haml::Util.to_hash(
Sass::SCSS::RX.constants.map {|c| [Sass::SCSS::RX.const_get(c), c.downcase]}).
merge(IDENT => "identifier", /[;}]/ => '";"')
def tok?(rx)
@scanner.match?(rx)
end
def expr!(name)
(e = send(name)) && (return e)
expected(EXPR_NAMES[name] || name.to_s)
end
def tok!(rx)
(t = tok(rx)) && (return t)
name = TOK_NAMES[rx]
unless name
# Display basic regexps as plain old strings
string = rx.source.gsub(/\\(.)/, '\1')
name = rx.source == Regexp.escape(string) ? string.inspect : rx.inspect
end
expected(name)
end
def expected(name)
self.class.expected(@scanner, @expected || name, @line)
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
def tok(rx)
res = @scanner.scan(rx)
if res
@line += res.count("\n")
@expected = nil
if [email protected]? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
@strs.each {|s| s << res}
end
end
res
end
end
end
end
© 2015 - 2025 Weber Informatics LLC | Privacy Policy